Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java及JVM是如何识别重载、重写方法的?

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

作者头像
JavaEdge
发布于 2021-12-27 00:43:48
发布于 2021-12-27 00:43:48
1.2K00
代码可运行
举报
文章被收录于专栏:JavaEdgeJavaEdge
运行总次数:0
代码可运行

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

案例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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:用于调用动态方法 较为复杂

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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],它可不通过动态类型,直接确定目标方法。

调用指令的符号引用

编译过程中,我们并不知目标方法的具体内存地址。因此,Java编译器会暂时用符号引表示该目标方法。 这符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。

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

  • 接口符号引用
  • 非接口符号引用
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在奸商.class的常量池中,#16为接口符号引用,指向接口方法"客户.isVIP()"。#22为非接口符号引用,指向静态方法"奸商.价格歧视()"。
$ javap -v 奸商.class ...
Constant pool:
...
  #16 = InterfaceMethodref #27.#29        // 客户.isVIP:()Z
...
  #22 = Methodref          #1.#33         // 奸商.价格歧视:()D
...

执行使用了符号引用的字节码前,JVM需解析这些【符号引用】并替换为【实际引用】。

对【非接口符号引用】,假定该【符号引用】所指向的类为C,则JVM按如下步骤查找:

  • 在C中查找符合名字及描述符的方法
  • 若没找到,搜索C的父类,直至Object类
  • 若还没找到,在C所直接实现或间接实现的接口中搜索,该步搜索得到的目标方法必须是非private、非static且若目标方法在间接实现的接口中,则需满足C与该接口间无其他符合条件的目标方法。若有多个符合条件的目标方法,则返回其中任一。

所以static方法也可通过子类来调用。子类的static方法会隐藏(这不是重写)父类中的同名、同描述符的静态方法。

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

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

经过上述解析步骤后,符号引用会被解析成实际引用:

  • 对可静态绑定的方法调用,实际引用是个指向方法的指针
  • 对需动态绑定的方法调用,实际引用则是个方法表的索引

总结与实践

本文介绍了Java以及Java虚拟机是如何识别目标方法的。

在Java方法的:

  • 重载,方法名相同而参数类型不相同的方法间
  • 重写,方法名相同&参数类型也相同的方法间

JVM识别方法的方式除了方法名和参数类型,还有返回类型。

JVM的:

  • 静态绑定:在解析时便能够直接识别目标方法的情况
  • 动态绑定,需在运行过程中根据调用者的动态类型来识别目标方法的情况。由于Java编译器已区分重载方法,因此可认为JVM不存在重载

在class文件中,Java编译器会用符号引用指代目标方法。在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。

Java的重写与Java虚拟机中的重写并不一致,但编译器会通过生成桥接方法来弥补。

参考

  • [1] https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html
  • [2] https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html
  • [3] https://wiki.openjdk.java.net/display/HotSpot/VirtualCalls
  • [4] https://wiki.openjdk.java.net/display/HotSpot/InterfaceCalls
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/12/25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
浅谈JAVA中静态绑定和动态绑定(源自《深入理解Java虚拟机》)
静态绑定:又称“前期绑定”,发生在编译期; 主要是方法重载(overload); 在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载版本。  动态绑定:又称“后期绑定”,发生在运行期; 主要是方法重写(override); 在运行阶段,Java虚拟机根据参数的实际类型决定调用哪个重写版本,查找的顺序是从子类->父类,直到找到该方法的声明为止;如果在层次结构的任何类中都找不到该方法,则虚拟机抛出错误信息。
用户7886150
2020/12/08
6130
Java 虚拟机-JVM是如何执行方法调用的?(上)
前不久在写代码的时候,我不小心踩到一个可变长参数的坑。你或许已经猜到了,它正是可变长参数方法的重载造成的。(注:官方文档建议避免重载可变长参数方法,见 [1] 的最后一段。)
码农架构
2021/02/07
1.5K0
Java 虚拟机-JVM是如何执行方法调用的?(上)
JVM精通面试系列 | 掘金技术征文
JRE 仅包含运行 Java 程序的必需组件,包括 Java 虚拟机以及 Java 核心类库等。我们 Java 程序员经常接触到的 JDK(Java 开发工具包)同样包含了 JRE,并且还附带了一系列开 发、诊断工具。
蒋老湿
2020/03/27
8180
JVM精通面试系列 | 掘金技术征文
JVM学习笔记
  java引用类型分为四种:类、接口、数组类和泛型参数。其中泛型参数会在编译过程中被擦除。因此 Java 虚拟机实际上只有前三种。在类、接口和数组类中,数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流(接口,类)。
良辰美景TT
2018/09/11
8790
JVM学习笔记
Java 基础面试题总结
hey guys ,这不是也到了面试季了么,cxuan 又打算重新写一下 Java 相关的面试题,先从基础的开始吧,这些面试题属于基础系列,不包含多线程相关面试题和 JVM 相关面试题,多线程和 JVM 的我放在后面了,下面不多说,搞起!
cxuan
2021/04/21
7340
Java 基础面试题总结
面试系列之-多态JVM的实现原理(JAVA基础)
一个对象变量可以指示多种实际类型的现象称为多态;允许不同类的对象对同一消息做出响应。方法的重载、类的覆盖(继承和实现)正体现了多态;
用户4283147
2023/08/21
2980
面试系列之-多态JVM的实现原理(JAVA基础)
JVM执行方法调用(一)- 重载与重写
JVM是怎么处理重载的?其实是编译阶段编译器就已经决定好调用哪一个重载方法。看下面代码:
颇忒脱
2019/03/13
4870
JVM执行方法调用(一)- 重载与重写
Java多态实现原理
##前言 多态是Java语言重要的特性之一,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。Java对于方法调用动态绑定的实现主要依赖于方法表,但通过引用调用(invokevitual)和接口引用调用(invokeinterface)的实现则有所不同。
全栈程序员站长
2022/09/08
5790
Java 小白成长记 · 第 7 篇「区分重载和重写,轻松掌握 Java 多态」
陆续讲完了抽象、封装和继承,终于到多态了,说实话这四个概念的耦合性比较高,明明每个概念都很清晰明了,但是拆分开来就确实不太好写,每篇写之前都需要构思很久。OK,本章写完面向对象的基本特征就全部结束喽,作为开胃小菜,接下来才是 Java 漫漫征程的开始。
飞天小牛肉
2021/02/26
5900
Java 小白成长记 · 第 7 篇「区分重载和重写,轻松掌握 Java 多态」
Java动态绑定与静态绑定之胡思乱想
之所以写这篇博客,是因为写代码过程中遇到了很奇怪的现象,我觉得只能通过动态绑定与静态绑定来解释,于是,就学习了一下动态绑定与静态绑定的实现原理,这个过程中确实学到了很多,怕以后忘了,所以用博客的形式记录下来。  为啥叫胡思乱想呢,是因为这篇博客主要记录的是我学到的内容和我的一些疑问与解答,并没有很强的逻辑性,所以就叫胡思乱想啦!
用户7886150
2021/02/02
1.1K0
重写与重载(Java)
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
用户10921393
2024/01/23
2140
Java常见面试题及答案
Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。 Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。 Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
Java编程指南
2019/08/02
5770
【面试题精讲】Java重载和重写有什么区别?
在上面的示例中,Calculator 类定义了两个名为 add 的方法,一个接受两个整数参数,另一个接受两个浮点数参数。通过重载,我们可以根据不同的参数类型来调用适合的方法。
程序员朱永胜
2023/10/05
3600
java 实现多态_Java多态的实现原理
多态的使用大家应该都比较了解,但是多态的实现原理就有点抽象了,查了很多很多资料,连续几天断断续续的看,有时候看着看着就走神了。毕竟太抽象,哈哈~
全栈程序员站长
2022/09/08
1K0
【Java探索之旅】多态:重写、动静态绑定
也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
屿小夏
2024/07/09
1240
JVM的类加载机制
其中,加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始,而类的解析不一定,类的解析可能在初始化阶段之后再开始,这是为了支持Java语言的动态绑定
大大大大大先生
2018/09/04
1.3K0
JVM的类加载机制
八股文-方法的重载与重写
在 Java 中,重载和重写是两个关键的面向对象编程概念。重载通过方法的参数列表不同来区分同名方法,提供了更灵活的方法调用方式。而重写通过子类重新定义父类中已经存在的方法,实现了多态性的体现,让代码更具可扩展性和维护性。
修己xj
2023/11/30
1920
八股文-方法的重载与重写
java多态理解和底层实现原理剖析
抽象事务的多种具体表现,称为事务的多态性。我们在编码过程中通常都是面向接口,面向抽象编程,这其实就利用了多态的好处,帮我们屏蔽了多个子类之间的实现差异。
大忽悠爱学习
2023/02/27
9260
java多态理解和底层实现原理剖析
Java 虚拟机:JVM是如何执行方法调用的?(下)
我在读博士的时候,最怕的事情就是被问有没有新的 Idea。有一次我被老板问急了,就随口说了一个。
码农架构
2021/02/07
1.3K0
Java 虚拟机:JVM是如何执行方法调用的?(下)
方法调用:一看就懂,一问就懵?
首先大家要明确一个概念,此处的方法调用并不是方法中的代码被执行,而是要确定被调用方法的版本,即最终会调用哪一个方法。
用户8639654
2021/07/30
3940
相关推荐
浅谈JAVA中静态绑定和动态绑定(源自《深入理解Java虚拟机》)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验