Java及JVM是如何识别重载、重写方法的?(上)

简介: Java及JVM是如何识别重载、重写方法的?

可变长参数方法的重载造成的。(官方文档建议避免重载可变长参数方法,见[1]的最后一段。

案例

void invoke(Object obj, Object... args) { ... }
void invoke(String s, Object obj, Object... args) { ... }
invoke(null, 1);    // 调用第二个invoke方法
invoke(null, 1, 2); // 调用第二个invoke方法
invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,
                               // 才能调用第一个invoke方法

某API定义了两个同名重载方法:

  • 第一个接收一个Object,以及声明为Object…的变长参数
  • 第二个则接收一个String、一个Object,以及声明为Object…的变长参数


想调用第一个方法,传参(null, 1),即声明为Object的形式参数所对应的实际参数为null,而变长参数则对应1。

之所以不提倡可变长参数方法重载,是因为Java编译器可能无法决定应该调用哪个目标方法。

这种情况下,编译器会报错,并且提示这方法调用有二义性。然而,Java编译器直接将我的方法调用识别为调用第二个方法,这究竟是为什么呢?


Java虚拟机是怎么识别目标方法的?

重载与重写

同一类中出现多个:


  • 名字相同
  • 参数类型相同


的方法,则无法编译。如想在同一个类中定义名字相同方法,它们参数类型必须不同。这些方法之间的关系称为重载。


这限制可通过字节码工具绕开,编译完成后,可再向class文件中添加方法名和参数类型相同,而返回类型不同的方法。当这种包括多个方法名相同、参数类型相同,而返回类型不同的方法的类,出现在Java编译器的用户类路径上时,它是怎么确定需要调用哪个方法的呢?

当前版本的Java编译器会直接选取第一个方法名以及参数类型匹配的方法。并且,它会根据所选取方法的返回类型来决定可不可以通过编译,以及需不需要进行值转换等。

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


  • 在不考虑对基本类型自动装拆箱及可变长参数情况下选取重载方法
  • 如在第1个阶段没找到适配方法,那在允许自动装拆箱,但不允许可变长参数情况下选取重载方法
  • 如在第2个阶段中没找到适配方法,那在允许自动装拆箱及可变长参数情况下选取重载方法


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


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

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


若子类定义与父类中非private方法的同名方法,且这两方法参数类型相同,那这俩方法间啥关系:


  • 若这俩都是static方法,那子类中的方法隐藏了父类中的方法
  • 若都不是 static 的,则子类的方法重写了父类中的方法


Java的方法重写是多态的体现:允许子类在继承父类部分功能同时,拥有自己独特行为。

重写调用会根据调用者的动态类型选取实际的目标方法。

JVM的静态绑定和动态绑定

Java虚拟机识别方法的关键在于类名、方法名及方法描述符(method descriptor)。

方法描述符由方法的参数类型及返回类型构成。

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

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


JVM方法重写判定同样基于方法描述符。

如子类定义了与父类中非私有、非静态方法同名的方法,则仅当这俩方法的参数类型及返回类型一致,JVM才会判定为重写。


对Java中重写而Java虚拟机中非重写的情况,编译器会通过生成桥接方法[2]实现Java的重写语义。


由于对重载方法的区分在编译阶段已完成,可认为JVM不存在重载概念。因此,某些文章将


  • 重载称为静态绑定(static binding)或编译时多态(compile-time polymorphism)
  • 重写称为动态绑定(dynamic binding)


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

JVM的:


  • 静态绑定指在解析时便能够直接识别目标方法
  • 动态绑定指要在运行过程中,根据调用者的动态类型来识别目标方法


Java字节码中与调用相关的指令有:


  • invokestatic:调用静态方法
  • invokespecial:调用私有实例方法、构造器及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法
  • invokevirtual:用于调用非私有实例方法
  • invokeinterface:用于调用接口方法
  • invokedynamic:用于调用动态方法
  • 较为复杂


编译生成这四种调用指令的情况。

interface 客户 {
  boolean isVIP();
}
class 商户 {
  public double 折后价格(double 原价, 客户 某客户) {
    return 原价 * 0.8d;
  }
}
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.8d;
  }
}

“商户”类定义了一个成员方法,叫“折后价格”,它接收一个double类型参数及一个“客户”类型参数。

这里“客户”是个接口,定义了一个接口方法“isVIP”。


“奸商”类这个方法,首先调用客户#isVIP,该调用会被编译为invokeinterface指令


  • 若客户是VIP,则调用奸商类的一个名叫“价格歧视”的静态方法。该调用会被编译为invokestatic指令
  • 如客户不是VIP,则通过super调用父类的“折后价格”方法。该调用会被编译为invokespecial指令


在静态方法“价格歧视”会调用Random类的构造器。该调用会被编译为invokespecial指令。然后以这个新建Random对象为调用者,调用Random类中的nextDouble方法。该调用会被编译为invokevirutal指令。


对于invokestatic以及invokespecial而言,Java虚拟机能够直接识别具体的目标方法。


而对于invokevirtual以及invokeinterface而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。


如虚拟机能确定目标方法有且仅有一个,比如说目标方法被标记为final[3][4],它可不通过动态类型,直接确定目标方法。


目录
相关文章
|
1月前
|
Java
Java语言实现字母大小写转换的方法
Java提供了多种灵活的方法来处理字符串中的字母大小写转换。根据具体需求,可以选择适合的方法来实现。在大多数情况下,使用 String类或 Character类的方法已经足够。但是,在需要更复杂的逻辑或处理非常规字符集时,可以通过字符流或手动遍历字符串来实现更精细的控制。
223 18
|
1月前
|
Java 编译器 Go
【Java】(5)方法的概念、方法的调用、方法重载、构造方法的创建
Java方法是语句的集合,它们在一起执行一个功能。方法是解决一类问题的步骤的有序组合方法包含于类或对象中方法在程序中被创建,在其他地方被引用方法的优点使程序变得更简短而清晰。有利于程序维护。可以提高程序开发的效率。提高了代码的重用性。方法的名字的第一个单词应以小写字母作为开头,后面的单词则用大写字母开头写,不使用连接符。例如:addPerson。这种就属于驼峰写法下划线可能出现在 JUnit 测试方法名称中用以分隔名称的逻辑组件。
198 4
|
2月前
|
算法 安全 Java
除了类,Java中的接口和方法也可以使用泛型吗?
除了类,Java中的接口和方法也可以使用泛型吗?
135 11
|
1月前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
267 5
|
2月前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
3月前
|
算法 Java
Java语言实现链表反转的方法
这种反转方法不需要使用额外的存储空间,因此空间复杂度为,它只需要遍历一次链表,所以时间复杂度为,其中为链表的长度。这使得这种反转链表的方法既高效又实用。
370 0
|
3月前
|
存储 Java 数据处理
Java映射操作:深入Map.getOrDefault与MapUtils方法
结合 `getOrDefault`方法的简洁性及 `MapUtils`的丰富功能,Java的映射操作变得既灵活又高效。合理地使用这些工具能够显著提高数据处理的速度和质量。开发人员可以根据具体的应用场景选择适宜的方法,以求在性能和可读性之间找到最佳平衡。
177 0
|
6月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
653 55
|
1月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
215 4
|
1月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。