反编译细看Java的泛型类型擦除 core Java 读书笔记之泛型

core Java 读书笔记之泛型

  1. 虚拟机中没有泛型,只有普通的类和方法。

  2. 所有的类型参数都用它们的限定类型替换。

    简单例子:

    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
                      
            // ...
    }
    
  3. 桥方法被合成来保持多态

    例子如下:

    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
    

    但是对于虚拟机来说,虚拟机使用参数类型和返回类型确定一个方法,所以虚拟机可以正确处理这种情况。

  4. 为保持类型安全性,必要时插入强制类型转换。

    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
    
发布了76 篇原创文章 · 获赞 53 · 访问量 4165

猜你喜欢

转载自blog.csdn.net/qq_42254247/article/details/102993534