core Java 读书笔记之泛型
-
虚拟机中没有泛型,只有普通的类和方法。
-
所有的类型参数都用它们的限定类型替换。
简单例子:
public class Array<T> { private Object[] array; private int size; public Array(T[] array) { this.array = array; this.size = array.length; } public Array() { this.array = new Object[]{}; } public boolean add(T value) { if (value == null) { return false; } this.array[this.size] = value; this.size++; return true; } public T get(int index) { if (index < this.size) { return (T)this.array[index]; } else { return null; } } }
类型擦除之后变为
public class ObjectArray { private Object[] array; private int size; public ObjectArray(Object[] array) { // ...省略 } public ObjectArray() { this.array = new Object[]{}; } public boolean add(Object value) { // ...省略 } public Object get(int index) { // ...省略 } }
我们使用
javap -v
来反编译查看如下:// 查看set方法 public boolean add(T); // 参数描述为Object descriptor: (Ljava/lang/Object;)Z flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=2 0: aload_1 1: ifnonnull 6 4: iconst_0 5: ireturn 6: aload_0 7: getfield #3 // Field array:[Ljava/lang/Object; 10: aload_0 11: getfield #4 // Field size:I 14: aload_1 15: aastore 16: aload_0 17: dup 18: getfield #4 // Field size:I 21: iconst_1 22: iadd 23: putfield #4 // Field size:I 26: iconst_1 27: ireturn LineNumberTable: line 22: 0 line 23: 4 line 26: 6 line 27: 16 line 28: 26 LocalVariableTable: Start Length Slot Name Signature 0 28 0 this LBaseLearn/genericityTest/erasedTest/Array; 0 28 1 value Ljava/lang/Object; LocalVariableTypeTable: Start Length Slot Name Signature 0 28 0 this LBaseLearn/genericityTest/erasedTest/Array<TT;>; 0 28 1 value TT; StackMapTable: number_of_entries = 1 frame_type = 6 /* same */ Signature: #31 // (TT;)Z
如果有类型限定,例如:
public class Array<T extends Number> { // ...省略 }
则类型擦除后变为
public class ObjectArray { private Object[] array; private int size; public ObjectArray(Number[] array) { // ... } public ObjectArray() { // ... } public boolean add(Number value) { // ... } public Number get(int index) { // ... } }
查看反编译后的代码来证实
{ public BaseLearn.genericityTest.erasedTest.Array(T[]); // 参数为Number descriptor: ([Ljava/lang/Number;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 10 7: putfield #2 // Field DEFAULT_CAPACITY:I 10: aload_0 11: aload_1 12: putfield #3 // Field array:[Ljava/lang/Object; 15: aload_0 16: aload_1 17: arraylength 18: putfield #4 // Field size:I 21: return LineNumberTable: line 12: 0 line 10: 4 line 13: 10 line 14: 15 line 15: 21 // ...省略 public BaseLearn.genericityTest.erasedTest.Array(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 10 7: putfield #2 // Field DEFAULT_CAPACITY:I 10: aload_0 11: aload_0 12: getfield #2 // Field DEFAULT_CAPACITY:I 15: anewarray #5 // class java/lang/Object 18: putfield #3 // Field array:[Ljava/lang/Object; 21: return public boolean add(T); // 参数为Number descriptor: (Ljava/lang/Number;)Z flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=2 0: aload_1 1: ifnonnull 6 4: iconst_0 5: ireturn 6: aload_0 7: getfield #3 // Field array:[Ljava/lang/Object; 10: aload_0 11: getfield #4 // Field size:I 14: aload_1 15: aastore 16: aload_0 17: dup 18: getfield #4 // Field size:I 21: iconst_1 22: iadd 23: putfield #4 // Field size:I 26: iconst_1 27: ireturn // ... public T get(int); descriptor: (I)Ljava/lang/Number; flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: iload_1 1: aload_0 2: getfield #4 // Field size:I 5: if_icmpge 18 8: aload_0 9: getfield #3 // Field array:[Ljava/lang/Object; 12: iload_1 13: aaload 14: checkcast #6 // class java/lang/Number 17: areturn 18: aconst_null 19: areturn // ... }
-
桥方法被合成来保持多态
例子如下:
public class MyArrayList extends Array<Number> { public MyArrayList() { super(); } public boolean add(Number number) { System.out.println("子类调用number"); return true; } }
此时,如果我建立一个实例,编译。
public class Main { public static void main(String[] args){ MyArrayList list = new MyArrayList(); Array<Number> array = list; array.add(1); } }
使用
javap -v
进行反编译public BaseLearn.genericityTest.erasedTest.MyArrayList(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method BaseLearn/genericityTest/erasedTest/Array."<init>":()V 4: return LineNumberTable: line 11: 0 line 12: 4 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LBaseLearn/genericityTest/erasedTest/MyArrayList; public boolean add(java.lang.Number); descriptor: (Ljava/lang/Number;)Z flags: 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 子类调用number 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: iconst_1 9: ireturn LineNumberTable: line 15: 0 line 16: 8 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this LBaseLearn/genericityTest/erasedTest/MyArrayList; 0 10 1 number Ljava/lang/Number; // 从父类继承的Object方法 public boolean add(java.lang.Object); descriptor: (Ljava/lang/Object;)Z flags: 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/Number // 转向子类的Number方法 5: invokevirtual #6 // Method add:(Ljava/lang/Number;)Z 8: ireturn LineNumberTable: line 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this LBaseLearn/genericityTest/erasedTest/MyArrayList;
从反编译可以看出,因为继承了泛型类,所以类型擦除后,出现了这样的奇怪方法
public boolean add(Object number) { // ... }
显然这个方法和我们的方法不同,此时,因为要调用合适的方法,所以编译器在其父类会生成一个桥方法,用来调用子类的方法,类似于这样
public boolean add(Object number) { return this.add((Number)number); }
对于类型为
T
的方法类型也存在此问题,由于虚拟机使用参数类型和返回类型来确定一个方法,所以虚拟机讷讷狗狗正确处理此种情况。对当前代码增加
get
方法:public class MyArrayList extends Array<Number> { // ... 省略 @Override public Number get(int index) { System.out.println("子类调用get"); return 0; } }
修改
Main
测试,再编译public class Main { public static void main(String[] args){ MyArrayList list = new MyArrayList(); Array<Number> array = list; array.add(1); array.get(5); } }
再对
MyArrayList
进行反编译,会发现有两个get
方法public java.lang.Number get(int); descriptor: (I)Ljava/lang/Number; flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String 子类调用get 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: iconst_0 9: invokestatic #6 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 12: areturn LineNumberTable: line 22: 0 line 23: 8 LocalVariableTable: Start Length Slot Name Signature 0 13 0 this LBaseLearn/genericityTest/erasedTest/MyArrayList; 0 13 1 index I // 父类继承的类型擦除之后的Object返回类型方法 public java.lang.Object get(int); descriptor: (I)Ljava/lang/Object; flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 // 调用Number方法 2: invokevirtual #7 // Method get:(I)Ljava/lang/Number; 5: areturn LineNumberTable: line 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this LBaseLearn/genericityTest/erasedTest/MyArrayList;
所以相当于:
public class MyArrayList extends Array<Number> { // ... @Override public Number get(int index) { System.out.println("子类调用get"); return 0; } public Object get(int index) { /* 调用Number的该方法,此处不应这样写,但是没有想到好的写法 */ return get(index); } }
如果我们这样写
get
方法,编译时是会报错的,有如下提示Ambiguous method call. Both get (int) in MyArrayList and get (int) in MyArrayList match
但是对于虚拟机来说,虚拟机使用参数类型和返回类型确定一个方法,所以虚拟机可以正确处理这种情况。
-
为保持类型安全性,必要时插入强制类型转换。
MyArrayList list = new MyArrayList(); Array<Number> array = list; array.add(1); Number a = array.get(5);
此处由于编译时进行了类型擦除,所以原方法返回
Object
,所以此处代码需要强制类型向下转型,此代码相当于。// ...省略 Number number = (Number)array.get(0);
查看反编译后的
Main
代码即可知晓:(部分代码)// 调用Array的get 21: invokevirtual #6 // Method BaseLearn/genericityTest/erasedTest/Array.get:(I)Ljava/lang/Object; // 进行强制类型转换 24: checkcast #7 // class java/lang/Number