Java核心卷 卷一 自我笔记 5.2继承-------多态、方法调用、阻止继承和强制类型转换

多态
有一个用来判断是否应该设计为继承关系的简单规则,就是“is-a”规则,它表明子类的每个对象也是父类的对象。
“is-a”规则的另一种表述法是置换法则。它表明程序中出现父类对象的任何地方都可以用子类对象置换。例如,我们可以将一个子类对象赋给父类变量。

	Employee e;
	e=new Employee("aaa",....);
	e=new Manager("bbb",.....);

在Java中,对象变量是多态的。一个Employee 变量即可以引用一个Employee 变量,也可以引用一个Employee类的任何一个子类的对象(Manager) 。
然而,不能将父类的一个引用赋给子类变量,这种赋值是非法的。

Manager m=staff[i];

理解方法调用
弄清楚如何在对象上应用方法调用非常重要。下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:

  1. 编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一举例所有C类中名为f的方法和其父类中访问属性为public且名为f的方法。目前,编译器已经获得所有可能被调用的候选方法。
  2. 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析。例如,对于调用x.f(“hello”)来说,编译器会挑选x.f(String)而不是x.f(int)。由于允许类型转换,所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者编译器没有找到与参数类型匹配的方法,或者有多个匹配,那么会报错。
  3. 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式叫静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在我们的例子中,编译器就是采用动态绑定的方式生成一个条调用f.(String)的指令。
  4. 当程序运行,并采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最适合的那个类的方法。假设x的实际类型为D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的父类中去寻找,以此类推。、
    每次调用方法都要进行搜索,时间开销特别大。因此,虚拟机预先为每个类创建了一个方法表,其中列出来所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查这个表就行。我们以Manager方法表为例:
    Manager方法表,其中三个方法是继承而来,一个方法是重新定义的,还有一个方法是新增加的。
Manager:
getName()->Employee.getName()
getSalary()->Manager.getSalary()
getHireDay()->Employee.getHireDay()
raiseSalary()->Employee.raiseSalary()
setBonus()->Manager.setBonus()

事实上,上面列出的方法并不完整,因为我们省略了Object方法。
在运行时,调用e.getSalary()的解析过程为:

  1. 首先,虚拟机提取e的实际类型的方法表。即可能是Employee、Manager的方法表,也可能是Employee其他子类的方法表。
  2. 接下来,虚拟机搜索定义getSalary签名的类。此时,虚拟机已经知道应该调用哪个方法。
  3. 最后,虚拟机调用方法。

动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类aaa,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译。如果e恰好引用一个aaa类的对象,就会自动调用aaa.getSalary()方法。

在覆盖一个方法的时候,子类方法不能低于父类方法的可见性。特别是,如果父类方法是public,子类方法也一定要声明public。否则,有的时候会发生一些问题。

阻止继承:final类和方法
有时候,可以希望阻止人们利用某个类定义子类。不允许扩展的类我们称为final类,有final关键字修饰。例如:

public final class aaa extends Manager(){
 		.....
}

类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖整个方法。当然,域也可以声明为final。对于final域来说,构造对象之后,就不能改变它们的值了。不过,如果将一个类声明为final,只有其中的方法自动成为final。不包括域。
将方法或类声明为final的主要目的是:确保它们不会在子类中改变语义。

强制类型转换
之前,我们将一个类型强制转换成另一个类型的过程称为类型转换。比如说将浮点型强转为整数型,有时候,也可以将某个类的对象引用转换为另一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似。例如:

Manager boss=(Manager)staff[0];

进行类型转换的好处是:在暂时忽视对象的实际类型之后,使用对象的全部功能。例如,我们在上面的例子中,由于某些项是普通雇员,使用satff数组必须是Employee对象的数组,我们需要将数组质管部引用经理的元素复原成Manager类,以便能够访问新增加的所有变量。
在java中,每个对象变量都属于一个类型。类型描述了这个变量所引用的以及能够引用的对象类型。
将一个值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个父类变量,编译器是允许的。但是,将一个父类的引用赋个一个子类变量,必须进行类型转换,这样才能通过运行时的检测。
需要我们注意的是,在进行类型转换之前,先查看一下是否能够成功地转换。这个过程,使用instanceof操作符就可以实现。例如:

if(staff[1] instanceof Manager)
{	
	boss=(Manager)staff[1];
}

最后,如果这个类型转换不可能成功,编译器就不会进行这个转换。例如:

String c=(String)staff[1];

将会产生编译错误,这是因为String不是Employee的子类。
综上所诉

  • 只能在继承层次内进行类型转换。
  • 在将父类转换为子类之前,应该使用instanceof 进行检查。

事实上,通过类型转换调整对象的类型并不是一种好的做法。在我们写的例子中,大多数情况不需要将Employee对象转换为Manager对象,两个类的对象都能正确地调用getSalary方法,这是因为实现动态性的多态绑定机制能够自动地找到相应的方法。
只有在使用Manager中特有的方法时才需要进行类型转换。在一般情况下,应该尽量少用类型转换和instanceof 运算符。

发布了59 篇原创文章 · 获赞 6 · 访问量 989

猜你喜欢

转载自blog.csdn.net/weixin_43790623/article/details/102861146