再论向上转型
1)将对某个对象的引用视为对其基类型的引用的做法被称作“向上转型(upcasting)”――因为在继承树的画法中,基类是放置在上方的。
转机
1)将一个方法调用同一个方法主体关联起来被称作“绑定(binding)”。
若在程序执行前进行绑定(如果有的话,由编译器和链接程序实现),叫做“前期绑定(early binding)”。
在运行时,根据对象的类型进行绑定的办法叫做“后期绑定(late binding)”。后期绑定也叫做“动态绑定(dynamic binding)”或“运行时绑定(run-time binding)”。
2)Java 中除了static 和final 方法(private 方法属于final)之外,其他所有的方法都是后期绑定。
为什么要将某个方法声明为 final 呢?它可以防止其他人重载该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者是想告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final 方法调用生成更有效的代码。
是执行动态绑定还是静态绑定可以查看class字节码中方法的调用是invokevirtual指令(动态绑定),还是invokespecial指令(静态绑定)。下面是一个小例子:
public class A { public A() { print(); } //private方法默认就是final的,所以对于它的调用属于静态绑定,既没有多态效果,是通过invokespecial指令调用的 private void print() { } }
对应的class字节码(通过执行javap -c A得到):
Compiled from "A.java" public class A extends java.lang.Object{ public A(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: aload_0 5: invokespecial #2; //Method print:()V 8: return }
如果A类中的print方法是非final,非静态的:
public class A { public A() { print(); } //非private方法,非static方法,对于它的调用属于动态绑定,既会产生多态效果,是通过invokevirtual指令调用的 public void print() { } }
对应的字节码:
Compiled from "A.java" public class A extends java.lang.Object{ public A(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: aload_0 5: invokevirtual #2; //Method print:()V 8: return public void print(); Code: 0: return }
而如果是static方法,则是通过invokestatic指令调用的:
public class A { public A() { print(); } static void print() { } }
对应的字节码:
Compiled from "A.java" public class A extends java.lang.Object{ public A(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: invokestatic #2; //Method print:()V 7: return }
因为方法是动态绑定的,内部通过虚拟表(virture-table)来管理,从虚拟表中找到方法的地址,再到方法的地址里去取指令来执行。虚拟表可以看作一个类似于map的东西,它的key是方法的识别,value是方法的地址。假设父类中有一个非final,非static的print方法,那么父类的虚拟表有个key为print方法,value为父类的print方法的地址的信息;而子类对象的虚拟表,首先会加载父类的的方法,得到一个和父类一样的虚拟表信息,然后再加载自己的方法。如果子类重写了父类的print方法,可以知道子类虚拟表中key为print的信息重复了。由于子类的加载在后,所以会覆盖之前的print方法地址信息,所以此时调用子类对象方法的时候,就会找到子类对象的方法地址,再到子类对象的方法地址里去取指令,这就是动态绑定的原理。
注意虚拟表中只有非final,非static的方法才会出现,用invokestatic指令调用的类方法(static方法)不在这里出现,因为它们是静态的,不需要在方法表中间接指向;final的方法和实例的初始化方法(构造方法)不需要在这里出现,因为它们是被invokespecial指令调用的,所以也是静态绑定的。
3)只有非private 方法才可以被重载。
4)只有普通的方法调用可以是多态的;任何域访问操作都将由编译器解析,因此不是多态的;如果某个方法是静态的,它的行为就不具有多态性。
构造器和多态
构造器的调用顺序:
1. 调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等。直到最低层的导出类。
2. 按声明顺序调用成员的初始状态设置模块。
3. 调用导出类构造器的主体。
继承与清理
1)当覆盖继承类的方法时,务必记住调用基类版本相应方法。否则,基类的方法就不会生效。
构造器内部的多态方法的行为
1)如果要在构造器内部调用一个动态绑定的方法,就要用到那个方法被覆盖的定义。
2)初始化的实际过程
1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
2.调用基类构造器。(重复1,2步骤)
3. 按照声明的顺序调用成员的初始化代码。
4. 调用导出类的构造器主体。
3)因此,编写构造器时有一条有益的规则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”。在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private 方法,它们自动属于final 方法)。这些方法不能被重载,因此也就不会出现上述令人惊讶的问题。
Java SE5中添加了协变返回类型,它表示在导出类中的重写方法可以返回基类方法的返回类型的某种导出类型。
例如:
class Grain { public String toString() { return "Grain"; } }
class Wheat extends Grain { public String toString() { return "Wheat"; } }
class Mill { Grain process() { return new Grain(); } }
class WheatMill extends Mill { Wheat process() { return new Wheat(); } }
public class CovariantReturn { public static void main(String[] args) { Mill m = new Mill(); Grain g = m.process(); System.out.println(g); m = new WheatMill(); g = m.process(); System.out.println(g); } }