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 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
的代码:
反编译 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 ); }
invokevirtual
指令在代码中调用子类方法时会执行桥接方法:
1 2 3 4 5 public static void main (String[] args) { SuperClass c = new SubClass(); c.helloWorld(); }
编译 Test
的代码(加入上述 main
方法)。
反编译 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
方法,因此可以直接取用。
参考