更加详细的解释
举个栗子进行解释:
public class Test {
class A{
public int a = 1;
public void test(){
System.out.println("this is A method");
}
}
class B extends A{
public int a = 2;
public void test(){
System.out.println("this is B method");
}
}
public static void main(String []args){
//new了一个B 对象然后将其赋值给父类A
A a = new Test().new B();
//访问a中的变量a
System.out.println(a.a);
//调用a中的test方法
a.test();
}
}
输出结果:
1
this is B method
问题:为什么把一个子类的对象赋值给父类以后,访问其实例变量表现为父类的特征,当访问其方法时,又表现为子类的特征?
原因:
第一个问题:
A a = new Test().new B();
System.out.println(a.a);
首先,在这行代码中,我们要明白,A称为变量的静态类型,而B称为变量的实际类型。变量的静态类型在编译期就是可知的,而变量的实际类型是要在程序的运行期才能确定,编译器在编译程序的时候并不知道变量的实际类型是什么。
而当需要访问对象的一个实例变量的时候,是根据变量的静态类型决定的,这是发生在编译期的,在编译期是可知的。
第二个问题:
a.test();
接下来便是最复杂的问题,先用一句话来解释,当需要访问一个对象的方法时,是在运行期根据变量的实际类型来决定的。
我们使用javap生成这段代码的字节码,方便后面进行分析;
至于如何使用javap来生成代码的字节码,可以参考这篇文章。
使用Javap命令生成字节码
public class com.auto.demo.Test.Test {
public com.auto.demo.Test.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/auto/demo/Test/Test$B
3: dup
4: new #3 // class com/auto/demo/Test/Test
7: dup
8: invokespecial #4 // Method "<init>":()V
11: dup
12: invokevirtual #5 // Method java/lang/Object.getClass:()Ljava/lang/Class;
15: pop
16: invokespecial #6 // Method com/auto/demo/Test/Test$B."<init>":(Lcom/auto/demo/Test/Test;)V
19: astore_1
20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_1
24: getfield #8 // Field com/auto/demo/Test/Test$A.a:I
27: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
30: aload_1
31: invokevirtual #10 // Method com/auto/demo/Test/Test$A.test:()V
34: return
}
接下来我们解析一下这段字节码
public class com.auto.demo.Test.Test {
public com.auto.demo.Test.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
这一段是父类oobject(因为object对象是所有的没有显示继承的对象的父类)的初始化操作,调用object的init方法。
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/auto/demo/Test/Test$B
3: dup
4: new #3 // class com/auto/demo/Test/Test
7: dup
这段是分别创建来Test和B对象。因为是内部类所以需要这样来创建B对象。
中间省略一些,直接看最重要的一段字节码
24: getfield #8 // Field com/auto/demo/Test/Test$A.a:I
27: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
30: aload_1
31: invokevirtual #10 // Method com/auto/demo/Test/Test$A.test:()V
34: return
其中24行显示其访问的是A中的a变量。
但是,31行显示其调用的是A.test()方法,那为什么又调用的是B的test方法呢?这就是java多态的体现了。要想理解这个首先我们需要了解一下invokevirtual指令的多态查找,invokevirtual指令运行时解析过程大致分为以下几个步骤:
1、找到操作数栈顶的第一个元素所指向的对象的实际类型,计作c;
2、如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回Java.lang.IllegalAccessError异常;
3、否则,按照继承关系从下往上对c的各个父类进行第2步的搜索和验证过程;
4、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收的类型,所以最后访问的就是B中的test方法。