JVM是如何执行方法调用的(一)?

重载与重写

在Java程序里, 如果一个类中出现多个名称相同,并且参数类型相同的方法,那么它无法通过编译.在正常情况下, 如果想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同.这些方法之间的关系,我们称为重载.

重载的方法在编译过程中即可完成识别.具体到每一个方法调用, Java编译会根据所传入参数的声明类型来选择重载方法.选取的过程共分为三个阶段:

  • 在不考虑对基本数据类型自动拆装箱「auto-unboxing, auto-boxing」, 以及可变长参数的情况下选择重载方法.
  • 如果在1个阶段中没有找到适配的方法,那么在允许自动拆装箱,但是不允许可变长参数的情况下选择重载方法.
  • 如果在2个阶段中没有找到适配的方法,那么在允许自动拆装箱以及可变长参数的情况下选择重载方法.

如果Java编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴近的,而决定贴切程度的一个关键就是形式参数的类型的继承关系.

void test(Object obj, Object ... args){
    // do
}

void test(String str, Object obj, Object ... args){
   // do 
}

// 调用第二个test方法
test(null, 1);

// 调用第二个test方法
test(null, 1, 2);

// 如果想要调用第一个test方法, 只有手动绕开可变长参数的语法糖
test(null, new Object[]{1});

上述例子中, 当传入的参数为null时, 它既可以匹配第一个方法中声明的Object的形式参数, 也可以匹配第二个方法中声明的String的形式参数.由于String是Object的子类, 因此Java编译器会认为第二个方法更为贴切.

除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法,也就是说,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型不同, 那么大子类中, 这这两个方法同样构成的了重载.

那么,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型相同, 那么这两个方法之间又是什么关系呢?

如果这两个方法都是静态的, 那么子类中的方法隐藏了父类中的方法.如果两个方法都不是静态的, 且都不是私有的, 那么子类的方法重写了父类的方法.

Java重要的一个特性就是多态, 而方法的重写正是多态的最重要的一种方式: 它允许子类在继承父类部分功能的同时,拥有自己独特行为.

JVM的静态绑定和动态绑定

接下来, 来看看Java虚拟机是怎么识别方法的.

Java虚拟机识别方法的关键在于类名、方法名称、方法描述符「method descriptor」.

方法描述符, 它是由方法的参数类型及返回类型构成的.

在同一个类中, 如果同时出现多个名字相同且描述符也相同的方法,那么Java虚拟机会在类的验证阶段报错.

可以看到,Java虚拟机与Java语言不同, 它并不限制名字与参数类型的相同,但是返回类型的不同的方法出现在同一个类中,对于调用这些方法的字节码来说, 由于字节码所附带的方法描述符包含了返回类型, 因此Java虚拟机能够准确地识别目标方法.

Java虚拟机中关于方法重写的判定同样基于「方法描述符」.

也就是说, 如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型及返回值类型一致,Java虚拟机才会判定为重写.

对于Java语言中重写而Java虚拟机中非重写的情况,编译器会通过生成「桥接」的方法来实现Java中重写的语义.

由于对「重载方法」的区分在编译阶段已经完成,我们可以认为Java虚拟机不存在重载这一概念,因此, 「重载」也被称为「静态绑定(static binding)」, 或者「编译时多态(compile-time polymorphism)」;而「重写」则被称为「动态绑定(dynamic binding)」.

这种说法在Java虚拟机语境下并非完全正确. 这是因为某个类中的重载方法可能被它的子类所重写,因此Java编译器会将所有对非私有实例方法的调用编译为需要动态绑定的类型.

确切的说,Java虚拟机中「静态绑定」是指: 在解析时便能够识别目标方法的情况,而「动态绑定」则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况.

具体来说,Java字节码中与调用相关的指令共有五种:

  1. invokestatic, 用于调用静态方法.
  2. invokespecial, 用于调用私有实例方法、构造器、使用super关键字调用父类的实例方法、构造器、所实现接口的默认方法.
  3. invokevirtual, 用于调用非私有实例方法.
  4. invokeinterface, 用于调用接口方法.
  5. invokedynamic, 用于调用动态方法.

调用指令的符号引用

在编译过程中,我们并不知道目标方法的实际内存地址.因此Java编译器会暂时用「符号引用」来表示该目标方法.这一「符号引用」包括「目标方法所在的类或者接口名称及目标方法的方法名称和方法描述符」

「符号引用」存储在class文件的常量池中. 根据「目标方法是否为接口方法」, 这些引用可分为「接口符号引用」「非接口符号引用」.

在执行使用了符号引用的字节码前, Java虚拟机需要解析这些符号引用并替换为实际引用.

对于非接口的符号引用, 假定该符号引用所指向的类为C, 则Java虚拟机会按照如下步骤进行查找.

  1. 在C中查找符合名字及描述符的方法.
  2. 如果没有找到, 在C的父类中继续搜索, 直至Object类.
  3. 如果没有找到, 在C所直接实现或者间接实现的接口中搜索, 这一步搜索得到的目标方法必须是非私有的、非静态的. 并且, 如果目标方法在间接实现的接口中, 则需要满足C与该接口宰没有其他符合条件的目标方法.如果有多个符合条件的目标方法,则任意返回其中一个.

从这个解析算法来看, 静态方法也可以通过子类来调用.此外, 子类的静态方法会隐藏「注意与重写的区分」父类中同名、同描述符的静态方法.

对「接口符号引用」,假定该符号引用所指向的接口为I, 则Java虚拟机会按照如下步骤进行查找:

  1. 在I中查找符合名字及描述符的方法.
  2. 如果没有找到,在Object类中的仅有实例方法中搜索.
  3. 如果没有找到, 则在I的超接口中搜索. 「这一步搜索结果的要求与非接口符号引用步骤3的要求一致」

经过上述的解析步骤之后,「符号引用」会被解析成「实际引用」. 对于可以「静态绑定」的方法调用而言, 「实际引用」是一个指向方法的「指针」. 对于需要动态绑定的方法调用而言, 「实际引用」则是一个「方法表的索引」.

本篇完.

推荐大家“善知软件实训基地”在线学习视频:

https://www.bilibili.com/video/BV1vJ411C7wZ?from=search&seid=442038201265323950

 https://www.bilibili.com/video/BV1RJ41147qd?from=search&seid=6170019529066609126

猜你喜欢

转载自blog.csdn.net/itszt888/article/details/107835049