Kyle's Notebook

Java 多态、重写与桥接方法

Word count: 2.1kReading time: 9 min
2020/07/12

Java 多态、重写与桥接方法

“面向对象三大特性:封装、继承、多态”、“重写是多态最重要的体现方式”…

相信这些概念对于很多 Java 程序员而言早已是烂熟于心,而在 JVM 层面具体表现又是如何呢?

方法重写(Override)

相信平时写惯 Java 代码必然不会对 @Override 感到陌生了,其含义就是当前类的这个方法是对父类同名方法的 重写

Java 作为面向对象语言,其中最典型的特点就是 多态性

而作为多态的重要特性,重写是子类对从父类继承而来的功能进行改造、引入自身特性的方式。

如果子类定义了与父类中非私有方法同名同参的方法,而且该方法非静态,则子类重写(Override)了父类的该方法。

同名同参非私有非静态(静态的不行,相当于子类隐藏了父类方法)… 以上就是构成重写的条件。

显然这里面是不包括方法 返回类型 的,也就是说如果子类中出现了满足以上几点,且与父类方法相同、仅是返回类型不同的方法,是不能通过编译的:

被认为子类已经存在该方法,同名同参方法不允许同时出现在子类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test0 {
public static class SuperClass {
public void helloWorld() {
System.out.println("Parent: 'Hello, World!'");
}
}

public static class SubClass extends SuperClass {
@Override
public int helloWorld() {
System.out.println("Child: 'Hello, World!'");
return 0;
}
}
}
1
javac Test0.java
1
2
3
4
5
Test0.java:12: error: helloWorld() in SubClass cannot override helloWorld() in SuperClass
public int helloWorld() {
^
return type int is not compatible with void
1 error

这要求子类方法的返回类型,至少是要与父类方法的返回类型 兼容(比如子类方法的返回类型是父类方法的返回类型的子类)的,才能实现重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test1 {
public static class SuperClass {
public Number helloWorld() {
System.out.println("Parent: 'Hello, World!'");
return null;
}
}

public static class SubClass extends SuperClass {
@Override
public Integer helloWorld() {
System.out.println("Child: 'Hello, World!'");
return null;
}
}
}

这样就没问题了。

其中方法的入参、出参类型被称为 方法描述符(Method Descriptor),在 JVM 规范中有以下描述:

A method descriptor represents the parameters that the method takes and the value that it returns.

然而在 JVM 层面却没有这种限制,即同名同参不同返回类型的方法是可以出现在同一个类中的(字节码),因为它是基于类和方法名、方法描述符来识别方法的。

换而言之,在 JVM 层面判定子类方法对父类构成重写,还需要加上 相同的返回类型 这个条件。

桥接方法(Bridge Method)

因此存在 Java 层面实现了重写,但在 JVM 层面却不是重写的情况:比如上面的子类方法返回类型是 Integer,父类方法的返回类型是 Number

另一种是使用了泛型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test2 {
public static class SuperClass<T> {
public void helloWorld(T arg) {
System.out.println("Parent: 'Hello, World!'");
}
}

public static class SubClass extends SuperClass<Integer> {
@Override
public void helloWorld(Integer arg) {
System.out.println("Child: 'Hello, World!'");
}
}
}

这两种情况在 Java 层面都是重写,在 JVM 中却不是(1 是返回类型不同,2 是参数不同)。

返回类型不同

Test1 的代码进行编译,再反编译 SubClass

1
2
javac Test1.java
javap -v -c 'Test1$SubClass'
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
...
public java.lang.Integer helloWorld();
descriptor: ()Ljava/lang/Integer;
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Child: \'Hello, World!\'
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aconst_null
9: areturn
LineNumberTable:
line 14: 0
line 15: 8

public java.lang.Number helloWorld();
descriptor: ()Ljava/lang/Number;
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #5 // Method helloWorld:()Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 11: 0
...

可见在第 16 行中有提示:

1
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC

参考 JVM 规范可知该方法是桥接方法(ACC_BRIDGE),而且由编译器自动生成(ACC_SYNTHETIC)。

The ACC_BRIDGE flag is used to indicate a bridge method generated by a compiler for the Java programming language.

The ACC_SYNTHETIC flag indicates that this class or interface was generated by a compiler and does not appear in source code.

这是编译器为了保持 Java 重写的语义,在编译时自动生成的方法,使之在 JVM 层面也实现了重写。由于桥接方法的描述符与子类方法描述符(第 2 行)不同,JVM 可据此定位到具体方法。

泛型方法

Test2 中子类重写了父类的一个泛型方法,编译器也会在子类生成桥接方法。

编译 Test2 的代码:

1
javac Test2.java

反编译 SuperClass

1
javap -v -c 'Test2$SuperClass'
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
...
public com.ywh.test.Test2$SuperClass();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0

public void helloWorld(T);
descriptor: (Ljava/lang/Object;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Parent: \'Hello, World!\'
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
Signature: #14 // (TT;)V
...

可见泛型参数 T arg 在擦除后被修改成 Object

1
descriptor: (Ljava/lang/Object;)V

再反编译 SubClass

1
javap -v -c 'Test2$SubClass'
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
...
public void helloWorld(java.lang.Integer);
descriptor: (Ljava/lang/Integer;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Child: \'Hello, World!\'
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 14: 8

public void helloWorld(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #5 // class java/lang/Integer
5: invokevirtual #6 // Method helloWorld:(Ljava/lang/Integer;)V
8: return
LineNumberTable:
line 10: 0
}
...

编译器生成的桥接方法(第 16 行)的描述符是与父类方法一致的:

1
descriptor: (Ljava/lang/Object;)V

因此运行子类方法,实际上是执行桥接方法:

1
2
3
public void helloWorld(Object arg) {
System.out.println("Child: 'Hello, World!'");
}
1
2
3
4
5
public static void main(String[] args) {
SubClass subClass = new SubClass();
subClass.helloWorld(1);
// 输出“Child: 'Hello, World!'”
}

invokevirtual 指令

在代码中调用子类方法时会执行桥接方法:

1
2
3
4
5
public static void main(String[] args) {
SuperClass c = new SubClass();
c.helloWorld();
// 输出“Child: 'Hello, World!'”
}

编译 Test 的代码(加入上述 main 方法)。

1
javac Test1.java

反编译 Test1

1
javap -v -c 'Test1'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/ywh/test/Test1$SubClass
3: dup
4: invokespecial #3 // Method com/ywh/test/Test1$SubClass."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method com/ywh/test/Test1$SuperClass.helloWorld:()Ljava/lang/Number;
12: pop
13: return
LineNumberTable:
line 20: 0
line 21: 8
line 22: 13
...

上述代码在编译时对 helloWorld 方法的调用会被编译为 invokevirtual 指令(第 12 行)。

invokevirtual 指令是 JVM 用于调用非私有实例方法的,在执行时通常需要根据调用者的动态类型来确定具体的目标方法(除非在编译时就可以确定具体的类型)。

符号引用

SubClass 字节码的常量池中,可以看到 helloWorld 方法(第 7 行):

1
2
3
4
5
6
7
8
9
10
11
...
Constant pool:
#1 = Methodref #7.#17 // Test1$SuperClass."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // Child: \'Hello, World!\'
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Methodref #6.#23 // Test1$SubClass.helloWorld:()Ljava/lang/Integer;
#6 = Class #25 // Test1$SubClass
#7 = Class #28 // Test1$SuperClass
#8 = Utf8 <init>
...

这是指向 helloWorld 的非接口符号引用(Methodref,如果某个方法是由实现接口而来的,则为接口符号引用 InterfaceMethodref)。要执行符号引用的字节码,需要先根据方法名称和描述符查找到具体的类、转换为实际引用。

其查找的顺序是:子类(即本类) -> 父类(直到 Object 类) -> 子类实现的接口(直接或间接)。

如果在最后一步中有多个符合条件的目标方法,则取其中一个。

接口符号引用也是同理,只是如果在接口的实现类中找不到,则会在 Object 类的公有实例方法中查找。然后就是查找子类实现的超接口,以此类推。

在示例的 SubClass 中已经有 helloWorld 方法,因此可以直接取用。

参考

CATALOG
  1. 1. Java 多态、重写与桥接方法
    1. 1.1. 方法重写(Override)
    2. 1.2. 桥接方法(Bridge Method)
      1. 1.2.1. 返回类型不同
      2. 1.2.2. 泛型方法
    3. 1.3. invokevirtual 指令
    4. 1.4. 符号引用
    5. 1.5. 参考