为什么写这篇博客,之前对继承的理解知识大体理论上,最近有个同事问了个问题,发现对JAVA继承的底层实现相当模糊,结合《深入理解Java虚拟机:JVM高级特性与最佳实践》以及上网查的资料进行了一下深入学习。
程序:现在又两个父子类如下
class Parent{
public String str = "Parent";
private int a = 10;
public int getA() {
return a;
}
}
class Chield extends Parent {
public String str = "Chield";
private int a = 20;
public int getA() {
return a;
}
}
测试程序1:
public class TestInherit {
public static void main(String[] args) {
Parent p1 = new Parent();
Parent c1 = new Chield();
TestInherit.sayInherit(p1);
TestInherit.sayInherit(c1);
Parent p2 = new Parent();
Chield c2 = new Chield();
TestInherit.sayInherit(p2);
TestInherit.sayInherit(c2);
}
public static void sayInherit(Parent p) {
System.out.println("Call Parent");
}
public static void sayInherit(Chield c) {
System.out.println("Call Chield");
}
}
结果是:
Call Parent
Call Parent
Call Parent
Call Chield
首先介绍方法调用的四条字节码指令:
invokevirtual 调用对象的实例方法,根据对象的实际类型进行分配,
invokeinterface 调用由接口实现的方法,在运行时对象中找到相应的实现;
invokespecial 调用需要特殊处理的实例方法,即实例的初始化<init>、private方法或超类的方法;
invokestatic 调用静态方法(static方法)。
其余字节码操作指令不作介绍。
代码:
Parent c1 = new Chield();
中Parent 称为静态类型,Chield称为实际类型。
静态绑定:如果是private,static或者final方法或者构造器,那么编译时就可以准备知道应该调用哪个方法,这种调用方式成为静态绑定。对应的字节码指令是:invokespecila,invokestatic(应用:overload,由于发生在编译期所以voreload不是由虚拟机来执行的)
动态绑定:与静态绑定相对,方法调用在运行时才能决定的,就是动态绑定。对应的指令为:invokevirtual(应用:override)
invokevirtual指令的运行时解析过程大致如下:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型, 记作C
2)若果在类型C中找到常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,不通过则返回java.lang.IllegalAccessError异常,
3)否则,按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证,
4)如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError异常。
上边测试程序的子节码如下:
public class com.rt.TestInherit extends java.lang.Object{
public com.rt.TestInherit();
public static void main(java.lang.String[]);
Code:
0: new #16; //class com/rt/Parent
3: dup
4: invokespecial #18; //Method com/rt/Parent."<init>":()V #初始化
7: astore_1 #局部变量表 索引为1的位置
8: new #19; //class com/rt/Chield
11: dup
12: invokespecial #21; //Method com/rt/Chield."<init>":()V #初始化
15: astore_2 #局部变量表 索引为1的位置
16: aload_1 #加载局部变量表 索引为1的位置reference类型值到 操作数栈
17: invokestatic #22; //Method sayInherit:(Lcom/rt/Parent;)V #采用静态绑定
20: aload_2 #加载局部变量表 索引为2的位置reference类型值到 操作数栈
#采用静态绑定
,所以虽然c1变量的实际类型是Chiled,由于采用静态绑定方法参数是静态类型,因此输出“Call Parent”
21: invokestatic #22; //Method sayInherit:(Lcom/rt/Parent;)V
24: new #16; //class com/rt/Parent
27: dup
28: invokespecial #18; //Method com/rt/Parent."<init>":()V
31: astore_3
32: new #19; //class com/rt/Chield
35: dup
36: invokespecial #21; //Method com/rt/Chield."<init>":()V
39: astore 4
41: aload_3
42: invokestatic #22; //Method sayInherit:(Lcom/rt/Parent;)V
45: aload 4
47: invokestatic #26; //Method sayInherit:(Lcom/rt/Chield;)V
50: return
public static void sayInherit(com.rt.Parent);
Code:
0: getstatic #37; //Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #43; //String Call Parent
5: invokevirtual #45; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void sayInherit(com.rt.Chield);
Code:
0: getstatic #37; //Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #52; //String Call Chield
5: invokevirtual #45; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
主要疑点在红色标记处。
测试程序2:
public class TestInherit {
public static void main(String[] args) {
Parent p1 = new Parent();
Parent c1 = new Chield();
System.out.println(p1.str);
System.out.println(c1.str);
System.out.println(p1.getA());
System.out.println(c1.getA());
}
}
输出结果:
Parent
Parent
10
20
在Eclipse中,通过Debug查看c1中的属性如下图:
可以看出,子类中包含所有父类中的属性,这是由于在加载的时候会先加载父类。
System.out.println(p1.str);
System.out.println(c1.str);
对应字节码如下:
19: aload_1 #从局部变量表中加载父类到操作数栈
20: getfield #28; //Field com/rt/Parent.str:Ljava/lang/String;
23: invokevirtual #32; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: getstatic #22; //Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_2 #从局部变量表中加载子类类到操作数栈
30: getfield #28; //Field com/rt/Parent.str:Ljava/lang/String;
#调用invokevirtual指令,会采用动态分配,找到实际类型子类对象(new Chield()生成)
#但是由于属性是静态绑定,所以导致输出的父类的属性
#如果调用的是方法,那么就会通过动态类型绑定到子类对象上
33: invokevirtual #32; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: getstatic #22; //Field java/lang/System.out:Ljava/io/PrintStream;
原因是:在Java中,属性绑定到类型,方法绑定到对象!