一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情
本系列专栏:JVM专栏
前言
前面文章我们解析了Java字节码文件,其中介绍了各种常见的指令,其中说到调用方法时方法调用指令时,我们进行了跳过,所以本章开始,我们来仔细分析一波方法调用指令。
正文
理解方法调用指令很大一个好处就是理解Java程序和JVM中对方法的处理是有区别的,这样可以加深我们对Java程序的理解。
重载和重写
在Java程序中,如果一个类中出现多个名字相同,并且参数类型相同的方法,那么它是无法通过编译的,因为Java会认为这是同一个方法。在正常情况下,如果我们想在同一个类中定义名字相同的方法,那么它们的参数类型必须不同,而这些方法之间的关系,就叫做重载。
同样,重载也可以不在同一个类中的方法,比如子类定义了与父类中非私有方法同名的方法,而这2个方法的参数类型不同,那么在子类中,这2个方法同样构成了重载。
那么如果子类定义了与父类中非私有方法同名的方法,而且这2个方法的参数类型相同,那这2个方法之间是什么关系呢
这里要分情况,如果这2个方法都是静态的,那么子类中的方法会隐藏父类中的方法;如果这2个方法都不是静态的,且都不是私有的,那么子类的方法就是重写了父类的方法。
这就是Java程序中对于方法重载和重写的定义,也正是由于这些的存在,JVM才会有不同的调用指令,我们接着分析。
JVM的重写和重载
接着,我们看看JVM是怎么识别方法的。
JVM识别方法的关键在于类名、方法以及方法描述符,而方法描述符是由方法的参数类型以及返回类型所构成,所以在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么JVM会在类的验证阶段(链接的第一步骤)报错。
这里就会发现问题所在了,由于JVM和Java语言规则的不同,对于在Java语言中的重载在JVM中是不存在的,因为Java语言中重载的方法它的方法描述符必然不同;而对于重写,JVM中关于方法重写的判断同样是基于方法描述符,也就是说如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这2个方法的参数类型以及返回类型都不同时,JVM才判断为重写。
上面重写看起来没啥问题,但是别忘了方法描述符是包含返回类型的,也就是说下面情况:
//父类
public class People {
public Number getAge(){
return 30;
}
}
//子类
public class Student extends People{
@Override
public Integer getAge(){
return 18;
}
}
在这里会发现子类虽然重写了父类的getAge方法,但是返回类型却不是Number类型而是其子类型Integer,这种写法在Java语言中是合法的,但是JVM却认为是方法描述符不一样,那如何解决呢
JVM通过桥接方法来实现Java种的重写语义。
JVM桥接方法
桥接方法,顾名思义就是搭建一个桥,来达成一致,我们话不多说,直接看Student类的字节码:
public class com.wayeal.arithmeticapp.testOverride.Student extends com.wayeal.arithmeticapp.testOverride.People
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // com/wayeal/arithmeticapp/testOverride/Student
super_class: #5 // com/wayeal/arithmeticapp/testOverride/People
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #5.#15 // com/wayeal/arithmeticapp/testOverride/People."<init>":()V
#2 = Methodref #16.#17 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Methodref #4.#18 // com/wayeal/arithmeticapp/testOverride/Student.getAge:()Ljava/lang/Integer;
#4 = Class #19 // com/wayeal/arithmeticapp/testOverride/Student
#5 = Class #20 // com/wayeal/arithmeticapp/testOverride/People
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 getAge
#11 = Utf8 ()Ljava/lang/Integer;
#12 = Utf8 ()Ljava/lang/Number;
#13 = Utf8 SourceFile
#14 = Utf8 Student.java
#15 = NameAndType #6:#7 // "<init>":()V
#16 = Class #21 // java/lang/Integer
#17 = NameAndType #22:#23 // valueOf:(I)Ljava/lang/Integer;
#18 = NameAndType #10:#11 // getAge:()Ljava/lang/Integer;
#19 = Utf8 com/wayeal/arithmeticapp/testOverride/Student
#20 = Utf8 com/wayeal/arithmeticapp/testOverride/People
#21 = Utf8 java/lang/Integer
#22 = Utf8 valueOf
#23 = Utf8 (I)Ljava/lang/Integer;
{
public com.wayeal.arithmeticapp.testOverride.Student();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/wayeal/arithmeticapp/testOverride/People."<init>":()V
4: return
LineNumberTable:
line 3: 0
public java.lang.Integer getAge();
descriptor: ()Ljava/lang/Integer;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: bipush 18
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: areturn
LineNumberTable:
line 7: 0
public java.lang.Number getAge();
descriptor: ()Ljava/lang/Number;
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #3 // Method getAge:()Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 3: 0
}
会发现该字节码文件中,getAge方法居然有2个,我们先看第一个返回类型是Integer的方法,这个方法是我们在Java程序中定义的,会发现它是正常的调用,没有什么问题。
再看第二个返回类型是Number的方法,这个方法的flag中有个是ACC_BRIDGE标志,说明这个方法是Java编译器生成的,而我们会发现它的指令操作: #0 从本地变量表中取出this指针 #1 调用常量池中#3的方法,我们来看看这个常量池中#方法是啥:
#3 = Methodref #4.#18 // com/wayeal/arithmeticapp/testOverride/Student.getAge:()Ljava/lang/Integer;
会发现这个方法就是调用我们自己定义的返回Integer的方法,所以对于Java是重写但是JVM不是重写的情况,Java编译器会生成一个JVM符合的重写方法,然后在这个生成的桥接方法中调用Java中定义的重写方法。
总结
这篇文章很关键,必须要弄明白,因为它涉及了Java编译器生成关于方法调用指令的不同,我们下篇文章就来具体说一下到底有哪些方法调用指令以及区别。