JVM 寻找对应的方法

JVM 识别方法的关键在于一下三点:

  • 方法所在的类名
  • 方法名称
  • 方法描述符:由方法的参数类型,以及返回类型构成

(PS:在类的链接阶段中,会有一个验证过程,在验证过程中,如果出现多个名字相同,且描述符相同的方法,那么验证阶段就会报错)

JVM 中并不是 Java 代码,而是字节码,而调用方法的字节码携带着方法描述符,因为描述符中存在着详细的数据,所以 JVM 可以准确的找到对应的方法。

(再 PS:如果子类中定义了与父类相同的非私有,非静态的方法,若方法名,参数类型,返回类型相同,则 JVM 将子类中的这种方法判定为父类方法的重写;如果是重复的是静态方法,则是直接覆盖)

重载与重写

重载

JVM 根据传入方法的参数类型对重载方法进行选择,选取过程为:

  1. 不考虑自动装箱拆箱,不考虑可变长度参数,选取重载方法;
  2. 考虑自动装箱拆箱,不考虑可变长度参数,选取重载方法;
  3. 考虑自动装箱拆箱,考虑可变长度参数,选取重载方法;

如果 Java 编译器在同一个阶段中找到了多个重载方法,会选择一个更加确切的,而这个确切程度是由参数类型的继承关系决定的,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Test {

public static void test(Object obj) {
System.out.println("Object: " + obj);
}

public static void test(Integer i) {
System.out.println("Integer: " + i);
}

public static void test(String str) {
System.out.println("String: " + str);
}

public static void test(String str1, Object... objects) {
System.out.println("String1: " + str1);
System.out.println("Objects: " + Arrays.toString(objects));
}

public static void test(String str1, String... strings) {
System.out.println("String1: " + str1);
System.out.println("Strings: " + Arrays.toString(strings));
}

public static void main(String[] args) {
test(1.0);
System.out.println("=========================");
test(1);
System.out.println("=========================");
test("Tianyi");
System.out.println("=========================");
test("Tianyi", "Chen", "Tian", "Yi");
}
}

运行结果:

1
2
3
4
5
6
7
8
Object: 1.0
=========================
Integer: 1
=========================
String: Tianyi
=========================
String1: Tianyi
Strings: [Chen, Tian, Yi]

从这个例子中可以看到,传入的整型 1 和字符串 Tianyi 进入的重载方法并不是形参为 Object 的方法,因为 StringObject 的子类,对于传进来的 Tianyi 字符串更加确切,所以 JVM 就选择了 String 为形参的重载方法。

除了同一个类中的方法重载,还有继承父类的子类的方法重载,如果子类定义了与父类非私有非静态且名字相同参数类型不同的方法,则在子类中,这两个方法重载,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Parent {
public void test(String str) {
System.out.println("String: " + str);
}
}


public class Children extends Parent{
public void test(Integer i) {
System.out.println("Integer: " + i);
}
}


public class Test {
public static void main(String[] args) {
Parent p = new Children();
p.test("Tianyi");
Children c = new Children();
c.test("Tianyi");
c.test(1);
}
}

运行结果:

1
2
3
4
String: Tianyi
# 这两个结果出现了重载
String: Tianyi
Integer: 1

重写

借用上面父子类继承的例子,如果这两个方法是静态的,则子类的方法隐藏了父类的方法。如果这两个方法都不是静态的,且都不是私有的,则是子类重写了父类的方法。

静态绑定和动态绑定

重载被称为静态绑定或者编译时多态,重写被称为动态绑定或者运行时多态。

来自网络:JVM 中静态绑定指的是在解析时便能直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。

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

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

对于不同的指令,JVM 识别方式不同:

  • 对于 invokestatic 和 invokespecial,JVM 能够在解析的时候识别具体的方法,属于静态绑定;
  • 对于 invokevirtual 和 invokeinterface,JVM 需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法;

符号引用调用指令

JVM 加载类 一文中,写了在 class 加载到 JVM 前,类中对于其他类、方法或者字段是不知道具体地址的,甚至不知道自己的方法和地址;所以编译器会给这些成员生成一个符号引用。

符号引用存储在 class 常量池中,分为 接口符号引用非接口符号引用

非接口符号引用搜索方法:

  1. 在当前类中搜索与描述符相同的方法;
  2. 查找父类一直到 Object 中搜索与描述符相同方法;
  3. 在该类实现的接口或间接实现的接口中搜索,如果有多个,任意返回一个;

接口符号引用搜索方法:

  1. 在当前接口中搜索指定的方法;
  2. 查找 Object 中是否存在满足条件的方法
  3. 在超接口中搜索指定的方法;

经过上述步骤后,符号引用会被解析成实际引用,也就是一个指向方法的指针。