本文将从JVM字节码的角度解释方法重载与方法重写
方法重载
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package me.zhongmingmao.test;
public class StaticDispatch { interface Human { }
static class Man implements Human { }
static class Woman implements Human { }
public static void sayHello(Human human) { System.out.println("Hello , I'm a human"); }
public static void sayHello(Man man) { System.out.println("Hello , I'm a man"); }
public static void sayHello(Woman woman) { System.out.println("Hello , I'm a woman"); }
public static void main(String[] args) { Human man = new Man(); Human woman = new Woman();
sayHello(man); sayHello(woman); sayHello((Man) man); sayHello((Woman) woman); } }
|
运行结果
1 2 3 4
| Hello , I'm a human Hello , I'm a human Hello , I'm a man Hello , I'm a woman
|
分析
静态类型 VS 实际类型
静态类型在编译期可知,实际类型需要到运行期才可确定
- 例如sayHello(Human human),编译期只知道传进来的参数是Human类型的实例,但只有到了运行期才知道实际传进来的是Man类型实例还是Woman类型实例(或者其他Human实现类的实例)
重载判定
编译期在重载时是通过参数的静态类型而不是实际类型作为判断依据
- 因此在
编译期会依据参数的静态类型来决定使用哪一个重载版本
字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| # javap -v public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #7 // class me/zhongmingmao/test/StaticDispatch$Man 3: dup 4: invokespecial #8 // Method me/zhongmingmao/test/StaticDispatch$Man."<init>":()V 7: astore_1 8: new #9 // class me/zhongmingmao/test/StaticDispatch$Woman 11: dup 12: invokespecial #10 // Method me/zhongmingmao/test/StaticDispatch$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokestatic #11 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Human;)V 20: aload_2 21: invokestatic #11 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Human;)V 24: aload_1 25: checkcast #7 // class me/zhongmingmao/test/StaticDispatch$Man 28: invokestatic #12 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Man;)V 31: aload_2 32: checkcast #9 // class me/zhongmingmao/test/StaticDispatch$Woman 35: invokestatic #13 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Woman;)V 38: return LocalVariableTable: Start Length Slot Name Signature 0 39 0 args [Ljava/lang/String; 8 31 1 man Lme/zhongmingmao/test/StaticDispatch$Human; 16 23 2 woman Lme/zhongmingmao/test/StaticDispatch$Human;
|
其中4个调用sayHello方法的JVM字节码指令
1 2 3 4
| 17: invokestatic #11 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Human;)V 21: invokestatic #11 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Human;)V 28: invokestatic #12 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Man;)V 35: invokestatic #13 // Method sayHello:(Lme/zhongmingmao/test/StaticDispatch$Woman;)V
|
从字节码可以看出,在编译期,依据按照参数的静态类型已经明确选择了调用哪一个重载版本
方法重写
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package me.zhongmingmao.test;
public class DynamicDispatch {
interface Human { default void sayHello() { System.out.println("Hello , I'm a human"); } }
static class Man implements Human { @Override public void sayHello() { System.out.println("Hello , I'm a man"); } }
static class Woman implements Human { @Override public void sayHello() { System.out.println("Hello , I'm a woman"); } }
public static void main(String[] args) { Human man = new Man(); Human woman = new Woman();
man.sayHello(); woman.sayHello();
man = new Woman(); man.sayHello(); } }
|
运行结果
1 2 3
| Hello , I'm a man Hello , I'm a woman Hello , I'm a woman
|
分析
字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class me/zhongmingmao/test/DynamicDispatch$Man 3: dup 4: invokespecial #3 // Method me/zhongmingmao/test/DynamicDispatch$Man."<init>":()V 7: astore_1 8: new #4 // class me/zhongmingmao/test/DynamicDispatch$Woman 11: dup 12: invokespecial #5 // Method me/zhongmingmao/test/DynamicDispatch$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokeinterface #6, 1 // InterfaceMethod me/zhongmingmao/test/DynamicDispatch$Human.sayHello:()V 22: aload_2 23: invokeinterface #6, 1 // InterfaceMethod me/zhongmingmao/test/DynamicDispatch$Human.sayHello:()V 28: new #4 // class me/zhongmingmao/test/DynamicDispatch$Woman 31: dup 32: invokespecial #5 // Method me/zhongmingmao/test/DynamicDispatch$Woman."<init>":()V 35: astore_1 36: aload_1 37: invokeinterface #6, 1 // InterfaceMethod me/zhongmingmao/test/DynamicDispatch$Human.sayHello:()V 42: return LocalVariableTable: Start Length Slot Name Signature 0 43 0 args [Ljava/lang/String; 8 35 1 man Lme/zhongmingmao/test/DynamicDispatch$Human; 16 27 2 woman Lme/zhongmingmao/test/DynamicDispatch$Human;
|
关于字节码的执行过程请参考博文「字节码 - JVM字节码执行过程」,下面仅做简略说明
创建对象
指令0、3、4、7的主要作用是创建并初始化一个Man实例,并存入局部变量表中偏移为1的slot中(指令8、11、12、15作用类似)
0: new :为Man类型分配内存,并将引入压入操作数栈
3: dup :复制栈顶元素(即刚刚创建的Man引用),**再次入栈**,此时栈有2个一样的Man引用,主要是为了后面有2个出栈操作
4: invokespecial :弹出栈顶元素,即Man引用,调用<init>方法(即JVM为我们生成的**合成构造器**方法)
7: astore_1 :弹出栈顶元素,同样也是Man引用,并将其存入局部变量表中偏移为1的slot中
15: astore_2 :这条指令执行完以后,局部变量表中偏移为1的slot中存储的是man实例的引用,偏移为2的slot中存储的是woman实例的引用
多态查找
这里仅分析第一个man.sayHello();,其他的都是类似的原理
16: aload_1 : 将局部变量表中偏移为1的slot中的值压入到操作数栈中,此时栈顶元素为man实例的引用
17: invokeinterface :弹出栈顶元素,并调用接口方法(中间还有校验等步骤,这里忽略),即调用Man类型的实现
JVM字节码指令的执行伴随着操作数栈的出栈和入栈操作,多态调用也是在运行期才能确定调用的是哪一个重写版本