写在前面

本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和文献引用请见100个问题搞定Java虚拟机

解答

在 Java 中,方法存在重载以及重写的概念,重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同并且参数类型也相同的方法之间的关系。
Java 虚拟机识别方法的方式略有不同,除了方法名和参数类型之外,它还会考虑返回类型。
在 Java 虚拟机中,静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。
由于 Java 编译器已经区分了重载的方法,因此可以认为 Java 虚拟机中不存在重载。
在 class 文件中,Java 编译器会用符号引用指代目标方法。在执行调用指令前,它所附带的符号引用需要被解析成实际引用。
对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。

补充

重载

在 Java 程序里,如果同一个类中出现多个名字相同,并且参数类型相同的方法,那么它无法通过编译。

也就是说,在正常情况下,如果我们想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同。这些方法之间的关系,我们称之为重载。

重载方法的选择

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

选取的过程共分为三个阶段:

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

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

重载与继承

除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。

也就是说,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载。

重写

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

如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法。

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

众所周知,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 编译器会暂时用符号引用来表示该目标方法。

这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。

非接口符号引用

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

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

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

接口符号引用

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

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

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

对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。

对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。

上一篇 下一篇