某次逛论坛时发现一个非常有意思的题目,如下:
class A<B>
{
public String show(A obj)
{
return ("A and A");
}
public String show(B obj)
{
return ("A and B");
}
}
class B extends A
{
public String show(B obj)
{
return ("B and B");
}
public String show(A obj)
{
return ("B and A");
}
}
A a = new B();
B b = new B();
System.out.println(a.show(b));
上面的代码正确的结果会输出B and A,刚开始看到的时候也感觉莫名其妙,实际上这里包含有不少知识点,稍微不留神就会弄错。题目本身输出的结果不重要,关键是我们要掌握里面的知识点,明白为什么会这样输出,最犀利的方式还是从字节码的角度来观察。同样javap命令输出上面代码的字节码。
Compiled from "Test.java"
class A extends java.lang.Object{
A();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public java.lang.String show(A);
Code:
0: ldc #2; //String A and A
2: areturn
public java.lang.String show(java.lang.Object);
Code:
0: ldc #3; //String A and B
2: areturn
Compiled from "Test.java"
class B extends A{
B();
Code:
0: aload_0
1: invokespecial #1; //Method A."<init>":()V
4: return
public java.lang.String show(B);
Code:
0: ldc #2; //String B and B
2: areturn
public java.lang.String show(A);
Code:
0: ldc #3; //String B and A
2: areturn
}
public static void main(java.lang.String[]);
Code:
0: new #2; //class B
3: dup
4: invokespecial #3; //Method B."<init>":()V
7: astore_1
8: new #2; //class B
11: dup
12: invokespecial #3; //Method B."<init>":()V
15: astore_2
16: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: aload_2
21: invokevirtual #5; //Method A.show:(LA;)Ljava/lang/String;
24: invokevirtual #6; //Method java/io/PrintStream.println:(Ljava/lang/Str
ing;)V
27: return
}
上面是类A,类B和main方法的字节码,先来看下类A的字节码,可以很神奇的发现show(B obj)这个方法不见了,其实代码为了增加复杂性,故意将该方法写成show(B obj),实际上类A是一个泛型类,这里用符号B来表示的,可以换成任意的其他符号。众所周知的是java中泛型是伪泛型,在编译期会发生一个叫做类型擦除的动作,关于泛型的类型擦除有很多可以讲的东西,建议读者自行去百度一下。因此这里发生类型擦出后,实际存在的方法为show(Object obj)。这里是非常关键的一点。
类B的字节码没有什么特殊的内容,只是定义了两个方法show(B),show(A),不过要注意的是会覆写类A中的show(A)方法,同时继承show(Object obj),因此类B中有三个方法。
然后再来看下main方法的字节码,一步一步的过:
new指令在堆中分配类B需要的内存并初始化成员变量为默认值,返回执行该地址的指针压栈。
dup指令复制当前栈顶的元素
invokespecial调用B的初始化函数,消耗一个栈顶元素
astore_1将栈顶元素弹出赋值给局部变量表的第二个变量这里是A a
然后后面类似的操作
astore_2将栈顶元素弹出赋值给局部变量表的第三个变量这里是B b
后面三条指令连续三个压栈操作先压入out变量,然后是a,最后是b
invokevirtual方法很关键,这里虽然写的是A.show(A),但是不得不先提及两个概念
概念一:静态绑定
静态绑定指的是在编译期间就已经确定了要调用的方法,private、static和final修饰的方法都是静态绑定的,注意在java中只有方法才有绑定的概念。
概念二:动态绑定
动态绑定指的是在运行时根据对象实际的类型去寻找要调用的方法。JAVA 虚拟机调用一个类方法时(静态方法),它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定,是多态的一种。
介绍完这两个概念接着看invokevirtual指令,由于引用的类型为A,因此会首先搜索A的方法表信息,发现show(A)方法最符合,所以这里编译的时候绑定到A.show(A),但是在运行中会发生动态绑定,当发现实际对象类型为B时,会在B的方法表中寻找最合适的方法,如果没找到则向上寻找父类中合适的方法,这里由于B覆写了父类的show(A)方法,因此会调用B的show(A)方法。
以上就是一道题目引出的知识点,包括字节码的解释,静态动态绑定,泛型擦除。