Java泛型原理篇: 类型擦除以及桥接方法

专栏文章导航

Java泛型入门篇: 泛型类、泛型接口以及泛型方法
Java泛型进阶篇: 无界通配符、上界通配符以及下界通配符
Java泛型原理篇: 类型擦除以及桥接方法


文章目录

  • 前言
  • 1. 类型擦除
    • 无界擦除
    • 上界擦除
    • 下界擦除
  • 2. 桥接方法
  • 3. 泛型与数组
  • 4. “破坏”泛型


前言

    文章开始之前首先还是先思考一个经典问题,以下程序的输出是什么

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.class == intList.class);

  大多数人可能会想,既然泛型可以“限定”list中元素的类型,证明两个list或多或少是有所区别的,那么两个不同泛型类型list的字节码文件应该也不是一个。但运行程序就会发现,控制台输出为true,这意味两个list的class地址都一样,为同一个字节码文件,这似乎与猜想的不太一样。

  这个试验也侧面反映出一个现象:泛型在运行时就不存在了。事实上也确实如此,那么是怎么做到在编译时能“限制”类型,而在运行时又没有了“限制”呢?

1. 类型擦除

  之前曾经介绍过,Java中的泛型是在1.5版本之后才有的特性,而为了兼容1.5之前版本即没有泛型的代码(如List),Java采用了类型擦除机制来解决这一问题。

  所谓的类型擦除(type erasure),指的是泛型只在编译时起作用,在进入JVM之前,泛型会被擦除掉,根据泛型定义的形式而被替换为相应的类型。这也说明了Java的泛型其实是伪泛型。

无界擦除

  当泛型类型被声明为一个具体的泛型标识,或一个无界通配符时,泛型类型将会被替代为Object。这也比较容易理解,如List,在获取元素的时候因为不能够确定具体的类型,所以只能使用Object来接收,在擦除的时候也是一样的道理,无法确定具体类型,所以擦除泛型时会将其替换为Object类型,如:

public class TestGeneric<T> {
  private T t;
  
  public TesetGeneric(T t) {
    this.t = t;
  }

  public static <E> E test(E e) {
    // Do something...
    return e;
  }
}

  将会被修改为

public class TestGeneric {
  private Object t;
  
  public TesetGeneric(Object t) {
    this.t = t;
  }

  public static Object test(Object e) {
    // Do something...
    return e;
  }
}

  可以使用反射来验证我们的结论是否正确:

public static void main(String[] args) throws Exception {
    TestGeneric<String> test = new TestGeneric("");
    Class<? extends TestGeneric> cls = test.getClass();
    Field t = cls.getDeclaredField("t");
    // 结果为class java.lang.class
    System.out.println(t.getType());
    
    // 这里参数只能传Object,传其他类型会报找不到该方法
    Method method = cls.getDeclaredMethod("test", Object.class);
    // 结果为class java.lang.class
    System.out.print(method.getReturnType());
}

上界擦除

  当泛型类型被声明为一个上界通配符时,泛型类型将会被替代为相应上界的类型。如List,程序并不能够确定具体的类型,只知道是Number或其子类,所以会擦除为Number类型,如

public class TestGeneric<T extends Number> {
  private T t;
  
  public TesetGeneric(T t) {
    this.t = t;
  }

  public static <E> E test(E e) {
    // Do something...
    return e;
  }
}

  将会被修改为

public class TestGeneric {
  private Number t;
  
  public TesetGeneric(Number t) {
    this.t = t;
  }

  public static Number test(Number e) {
    // Do something...
    return e;
  }
}

  如果有多个上界,则会转换为extends关键字后的第一个类型。

public interface Comedy {
}
public interface Chinese {
}
public class ChineseComedy implements Comedy, Chinese {
}
/**
 * @author beemo
 */
public class TestGeneric<T extends Comedy & Chinese> {

    private T t;

    public TestGeneric(T t) {
        this.t = t;
    }

    public <T extends Comedy & Chinese> T test(T t) {
        return t;
    }

    public static void main(String[] args) throws Exception {
        ChineseComedy movie = new ChineseComedy();
        TestGeneric<ChineseComedy> test = new TestGeneric(movie);
        Class<? extends TestGeneric> cls = test.getClass();
        Field t = cls.getDeclaredField("t");
        // class xxx.Comedy
        System.out.println(t.getType());

        // 这里参数只能传Comedy,传其他类型会报找不到该方法
        Method method = cls.getDeclaredMethod("test", Comedy.class);
        // class xxx.Comedy
        System.out.print(method.getReturnType());
    }
}
/**
 * @author beemo
 */
public class TestGeneric<T extends Chinese & Comedy> {

    private T t;

    public TestGeneric(T t) {
        this.t = t;
    }

    public <T extends Chinese & Comedy> T test(T t) {
        return t;
    }

    public static void main(String[] args) throws Exception {
        ChineseComedy movie = new ChineseComedy();
        TestGeneric<ChineseComedy> test = new TestGeneric(movie);
        Class<? extends TestGeneric> cls = test.getClass();
        Field t = cls.getDeclaredField("t");
        // class xxx.Chinese
        System.out.println(t.getType());

        // 这里参数只能传Chinese,传其他类型会报找不到该方法
        Method method = cls.getDeclaredMethod("test", Chinese.class);
        // class xxx.Chinese
        System.out.print(method.getReturnType());
    }
}

下界擦除

  下界通配符的擦除,同无界通配符,只能确定下界类型,但是上界类型无法确定,所以只能替换为Object

public class TestGeneric<T> {

    private T t;

    public TestGeneric(T t) {
        this.t = t;
    }

    public <T> T test(T t) {
        return t;
    }

    public static void main(String[] args) throws Exception {
        Number number = 1.1;
        TestGeneric<? super Integer> test = new TestGeneric<>(number);

        Class<? extends TestGeneric> cls = test.getClass();
        Field t = cls.getDeclaredField("t");
        // class java.lang.Object
        System.out.println(t.getType());

        // 这里参数只能传Object,传其他类型会报找不到该方法
        Method method = cls.getDeclaredMethod("test", Object.class);
        // class java.lang.Object
        System.out.print(method.getReturnType());
    }
}

2. 桥接方法

  先看以下代码

public interface Generic<T> {
  void test(T t);
}
public class SubGeneric implements Generic<String> {

  @Override
  public void test(String s) {
    System.out.println(s);
  }

  public static void main(String[] args) {
    Generic subGeneric = new SubGeneric();
    subGeneric.test("1");
  }
}

  由于类型擦除的存在,所有上例中接口的test方法到JVM中会被修改为

public void test(Object object);

  而实现类SubGeneric由于定义了的Generic的泛型类型为String,所以实现类中的test方法入参类型为String,这似乎与父接口中的定义不同,但编译器却没报错。

  在查看main方法,首先第一行,使用多态创建了一个子类对象,并使用父接口的引用去指向,然后第二行执行了test方法,按照我们以往的理解,这里入参的类型肯定是Object,因为类型擦除后,接口最终定义的就是Object
Java泛型原理篇: 类型擦除以及桥接方法_第1张图片
  由于是多态,最终会运行子类的test,程序运行后最终也能正确的运行,输出了传入的字符串1。那么如果传入的类型为其他类型,如整型的1,那么会发生什么呢。
Java泛型原理篇: 类型擦除以及桥接方法_第2张图片
  “矛盾”点似乎出现了。一是子类与父接口的方法参数类型不一致,二是调用时按理说传入任意类型都可以,但是实际却是传入非子类方法中定义的类型那么会有问题。

  使用反射来观察SubGeneric中的方法信息

Class<?> clz = SubGeneric.class;
Method[] declaredMethods = clz.getDeclaredMethods();

在这里插入图片描述
  可以看到,多了一个与父接口同样的Object类型参数的方法,而这个方法不是我们定义的,推断是自动生成的,我们在使用javap -v指令来查看一下汇编指令。
Java泛型原理篇: 类型擦除以及桥接方法_第3张图片
Java泛型原理篇: 类型擦除以及桥接方法_第4张图片

  首先可以看到test(Object)flagstest(String)flags多了两个值,分别为ACC_BRIDGEACC_SYNTHETIC。官网中描述,ACC_BRIDGE表示此方法为编译器生成的桥接方法ACC_SYNTHETIC表示此方法由编译器生成,并且在源代码中并不存在。这也就解释了为什么子类与父接口的方法类型不一致,但是程序却可以正常执行,因为编译器自动生成了一个与父类接口参数类型一致的方法。

  其次可以看到,生成的桥接方法中代码实现。首先先将参数强制转换为String类型,然后再调用test(String)方法,这就是为什么虽然可以传递非String类型的参数,但是却报转型异常的原因了。

  总结:桥接方法是编译器自动生成的方法,这个方法并不会在源代码中出现。桥接方法的目的是为了解决由于类型擦除特性导致的方法不一致问题。

  需要注意的是,除了此种情况以外,一些其他情况也可能会生成桥接方法,但这并不属于本篇文章的讨论范围,故不在此展开。

3. 泛型与数组

  有的时候可能想要在程序中定义泛型的数组,比如

Generic<String>[] generics = new Generic<String>[2];

  但是这种方式编译器会报错,有时候我们尝试了很多种方式,就是无法创建一个泛型数组,那么具体怎么创建呢?

  首先我们需要知道数组是具有协变性的,例如

Integer[] intArr = new Integer[2];
Object[] objArr = intArr;

  以上的写法是允许的,这就导致了问题的发生

objArr[0] = "123";

  以上代码编译可以通过,但是因为向一个Integer的数组中赋值了字符串类型,所以在运行时会报ArrayStoreException

  通过泛型的学习,我们知道泛型是不具备协变性的,ArrayList numbers = new ArrayList();这种写法无法通过编译。我们从一个泛型容器中获取具体元素的时候,并不需要强制转型,因为必然会获得想要的类型。

  结合这两个特点,我们就可以分析出我们想要的答案,那就是Java不支持泛型数组

  让我们来分析一下如果支持泛型数组会发生什么

ArrayList<Integer>[] intListArray = new ArrayList<Integer>[2];
ArrayList<Integer>[] intListArray2 = intListArray;
Object[] objArray = intListArray;

ArrayList<String> strs = new ArrayList<>();
strs.add("123");

objArray[0] = strs;
ArrayList<Integer> intList = intListArray2[0]; 
Integer a = intList.get(0); // ❶

  ①处按理说我们可以放心的获取一个Integer的元素,但是实际上获得的却是一个字符串类型,这就会导致类似转型异常的发生,泛型也就失去了意义。所以为了安全考虑,是不支持泛型数组的

4. “破坏”泛型

  正常情况下,我们是可以放心的使用从泛型容器中获得的元素的,如上文所说,可以确定得到的就是明确的类型。但是有一种情况除外,就是利用“万能”的反射。

  如同可以“破坏”单例一样,反射也可以“破坏”泛型。由于反射是运行时生效,而泛型类型的检查是在编译时,如果未定义上界,那么将会擦除为Object,即可以接受任意类型。所以利用反射,我们可以注入其他类型的值。
Java泛型原理篇: 类型擦除以及桥接方法_第5张图片

你可能感兴趣的:(泛型,泛型)