基本概念总结

符号引用在类的加载阶段或者第一次引用时转换成直接引用,这叫做静态解析。一些符号引用则在每次运行时转换成直接引用,这种转换是动态链接,java的多态正是在此时实现的。

方法调用的几个指令:

  • invokeinterface:调用接口中的方法
  • invokestatic:调用静态方法
  • invokespecial:调用自己的私有方法、构造方法和父类的方法
  • invokevitural:调用虚方法,运行期间动态查找虚方法
  • invokedynamic:动态调用方法

静态解析的四种情形:

  • 静态方法
  • 父类方法
  • 构造方法
  • 私有方法 以上四种方法称为非虚方法,他们是在类加载阶段将符号引用转换成直接引用。

重载的静态分派

如果类B继承类A,对于A a = new B();变量a的静态类型是A,变量a的实际类型是B。变量的静态类型永远不会改变,而变量引用所指向的实际类型可以发生变化。

对于如下例子:

 1package test;
 2
 3public class TestOverload {
 4    void test(GrandFather gf) {
 5        System.out.println("test GrandFather!");
 6    }
 7
 8    void test(Father f) {
 9        System.out.println("test Father!");
10    }
11
12    void test(Son s) {
13        System.out.println("test Son");
14    }
15
16    public static void main(String[] args) {
17        TestOverload testOverload = new TestOverload();
18        GrandFather g1 = new GrandFather();
19        GrandFather g2 = new Father();
20        GrandFather g3 = new Son();
21        testOverload.test(g1);
22        testOverload.test(g2);
23        testOverload.test(g3);
24
25    }
26}
27
28class GrandFather {
29}
30
31class Father extends GrandFather {
32}
33
34class Son extends Father {
35}

输出结果是:

1test GrandFather!
2test GrandFather!
3test GrandFather!
4
5Process finished with exit code 0

从输出结果可以看出,类的方法重载是一种静态分派,在编译时已经可以确定方法调用。也就是查找方法时,根据传入参数的静态类型来选定方法。本例中g1,g2和g3的静态类型都是GrandFather,所以TestOverload的第一个方法被调用。

重写的动态分派

重新是一种动态分派。

 1package test;
 2
 3
 4public class TestOverride {
 5    public static void main(String[] args) {
 6        Fruit apple = new Apple();
 7        apple.test();
 8
 9        Fruit orange = new Orange();
10        orange.test();
11    }
12}
13
14class Fruit {
15    void test() {
16        System.out.println("test Fruit");
17    }
18}
19
20class Apple extends Fruit {
21    @Override
22    void test() {
23        System.out.println("test Apple");
24    }
25
26}
27
28class Orange extends Fruit {
29    @Override
30    void test() {
31        System.out.println("test Orange");
32    }
33}

main方法的4行代码对于的字节码指令如下:

 1 0 new #2 <test/Apple>
 2 3 dup
 3 4 invokespecial #3 <test/Apple.<init>>
 4 7 astore_1
 5 8 aload_1
 6 9 invokevirtual #4 <test/Fruit.test>
 712 new #5 <test/Orange>
 815 dup
 916 invokespecial #6 <test/Orange.<init>>
1019 astore_2
1120 aload_2
1221 invokevirtual #4 <test/Fruit.test>
1324 return

解释如下:

  1. new #2 <test/Apple> 表示创建对象,该指令在堆上为该对象分配内存空间,并将地址压入操作数栈顶
  2. dup指令复制操作数栈顶的值,并将其压入栈顶,此时操作数栈上有两个连续的相同的对象地址
  3. invokespecial #3 <test/Apple.> 调用刚创建的实例的构造方法,因为是实例方法,所以需要从操作数栈顶弹出一个this引用,也就是说这一步会弹出一个之前入栈的对象地址
  4. astore_1 弹出栈顶元素存储到局部变量表中的一个变量
  5. aload_1 存储在局部变量表中的第一个变量的对象引用,将被压入栈顶
  6. invokevirtual #4 <test/Fruit.test> 调用虚方法,注意这里引用的是Fruit类的test方法,也就是引用变量的静态类型的方法。
  7. 后面的指令依此类推

invokevirtual指令查找方法过程:

  1. 到操作数栈顶寻找栈顶元素所指向的对象实际类型
  2. 如果实际类型中存在和静态类型方法的方法名和描述符都相同的方法(并且权限校验通过),则直接返回该目标方法的直接引用
  3. 如果不存在,则根据类的继承层次依次向上查找,如果最终没有找到,则抛出异常。

可见,方法的重载是编译期静态行为,而方法重写是运行期动态行为。