其实关于类加载机制和双亲委派在之前已经写过专栏了,链接:https://blog.csdn.net/qq_41936805/article/details/95814739,但是决定再总结一下。
先举一个关于变长参数的函数调用的例子:
void invoke(Object obj,Object ... args){}
void invoke(String s,Object obj,Object ... args){}
invoke(null,1);//调用第2个invoke方法
invoke(null,1,2);//调用第2个invoke方法
invoke(null,new Object[]{1});//只有手动绕开可变长参数的语法糖
//才能调用第一个invoke方法
这个语法糖我还是觉得挺有意思的,意思大家估计也看明白了,所以java官方文档是不建议这么去重载一个方法的,因为java编译器无法决定应该调用哪个目标方法。
在这种情况下,编译器会报错,并且提示这个方法存在二义性。然而,java编译器直接将其识别为调用第二个方法,这是为什么呢?
于是带着这个问题来看看jvm是如何识别目标方法的。
重载与重写
在Java程序中,如果同一个类中出现多个名字相同,并且参数类型相同的方法,那么它无法通过编译。也就是说,在正常情况下,如果我们想要在同一个类中定义名字相同的方法,那么他们的参数类型必须不同,这些方法之间的关系,称之为重载。
而重载的方法在编译的过程中可以完成识别。具体到每一个方法调用,Java编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段:
- 在不考虑对基本类型自动装拆箱,以及可变长参数的情况下,选择重载方法。
- 如果在第1个阶段中没有找到适配的方法,那么就允许自动装拆箱,但是不允许可变长参数的情况下选取重载方法。
- 如果在第2个阶段中没有找到适配的方法,那么就会允许自动装拆箱以及可变长参数的情况下选取重载方法。
如果匹配到了多个适配的方法,那么就会在其中选择一个最贴切的,而决定贴切程度的关键是形式参数类型的继承关系.
还用开头的例子来说:
void invoke(Object obj,Object ... args){}
void invoke(String s,Object obj,Object ... args){}
invoke(null,1);//调用第2个invoke方法
invoke(null,1,2);//调用第2个invoke方法
invoke(null,new Object[]{1});//只有手动绕开可变长参数的语法糖
//才能调用第一个invoke方法
当传入null的时候,它同时可以匹配obj和s,但是因为String是Object的子类,所以Java编译器会认为第二个更为贴切。
除了同一个类中的方法,重载也可以用于这个类所继承而来的方法,也就是说,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载。
那么,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型相同,那么又是什么关系呢?
如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法。如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法。
众所周知,Java是一门面向对象的编程语言,方法的Overide正是其基本特点多态的一种表现形式:它允许子类再继承父类部分功能的同时,拥有自己独特的行为。
JVM静态绑定与动态绑定
接下来,来看看JVM是如何识别方法的。
JVM识别方法的关键在于类名、方法名以及方法描述符(method descriptor)。前面两个就不做过多的解释了。至于方法描述符,它是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述符也有相同的方法,那么JVM会在类的验证阶段报错。
所以JVM与Java不同,因为它不限制名字和参数类型相同,但是返回类型不同的方法出现在同一个类中,对于调用这些方法的字节码来说,因为字节码附带的方法描述符包含了返回类型,因此JVM能够准确识别出目标方法。
JVM中关于Overide的判定也基于方法描述符。也就是说,如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,JVM才会判定为Overide。
如果java判定为重写,jvm不这么判定,那么编译器就会生成桥接方法来实现Java中的重写语意。
在这里,重载也可以被译为静态绑定,重写则被称为动态绑定。
再进一步确切的说:JVM的静态绑定指的是在解析的时候能够识别目标方法的情况,而动态绑定指的是在运行过程中根据调用的动态类型来识别目标方法的情况。
具体的太说,Java字节码中与调用相关的指令一共有五种:
- invokestatic:用于调用静态方法。
- invokespecial:用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
- invokevirtual:用于调用非私有实例方法。
- invokeinterface:用于调用接口方法。
- invokedynamic:用于调用动态方法。
在这里,我简单介绍一下前面四种:
interface 客户{
boolean isVIP();
}
public class 商户 {
public double 折后价格(double 原价, 客户 某客户){
return 原价 * 0.8f;
}
}
class 奸商 extends 商户{
@Override
public double 折后价格(double 原价, 客户 某客户) {
if (某客户.isVIP()) { //invokeinterface
return 原价 * 价格歧视(); //invokestatic
} else {
return super.折后价格(原价, 某客户); //invokespecial
}
}
public static double 价格歧视() {
//将客户城市作为随机数生成器的种子
return new Random() //invokespecial
.nextDouble() //invokevirtual
+ 0.8f;
}
}
调用指令的符号引用
在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java编译器会暂时用符号引用来表示该目标方法。这一引用包括目标方法说实在的类或者接口的名字,以及目标方法的方法名和方法描述符。
符号引用存储在class文件的常量池之中。先判断是否为接口,然后用不同的符号去引用。
如果是非接口符号引用,假定该符号的引用所指向的类为CLASS,则JVM会按照如下步骤进行查找:
- 在CLASS中查找符合名字及描述符的方法。
- 如果没找到,在C的父类中继续搜索,直至Object类
- 如果没有找到,在C所直接实现或者间接实现的接口中搜索中得到的方法必须是非私有,非静态的。并且,如果目标方法在间接实现的接口中,则需要满足C与该接口之间没有其他符合条件的目标方法,如果有多个符合条件的目标方法,就任意返回其中一个。
结论:静态方法也可以通过子类来调用。此外,子类的静态方法会隐藏父类中同名、同描述符的静态方法。
如果是接口引用,假定该符号引用所指向的接口为INTERFACE,则JVM会按照如下步骤进行查找:
- 在INTERFACE中查找符合名字和描述符的方法
- 如果没有找到,在Object类中的公有实例方法中搜索
- 如果没有找到,则在INTERFACE的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤3的要求一致。
结论:符号引用会被解析为实际引用。静态绑定就是一个指向方法的指针。动态绑定就是一个方法表的索引。然后下篇文章说什么是方法表。