泛型相关的总结

Generic in Java

Posted by LiuShuo on May 1, 2020

由于泛型是JDK1.5之后引入的,所以为了保持向前兼容字节码,JDK引入了类型擦除erasure的概念,它将泛型信息在变异后擦除掉 在运行时完全感知不到泛型的存在。本文主要对泛型引入的一些问题做一些总结归纳。

Java不允许创建泛型数组

首先想一下什么是「泛型数组」…,是不是一时想不出来?因为平时没怎么写过吧,为啥呢,因为压根就不支持,编译报错,如:

List[] arrayOfLists = new List[2]; // compile-time error

我们模拟了一个List集合的泛型为Integer的数组,很遗憾这个过不了编译器的检查。

为什么呢?

首先来看一下非泛型数组也就是我们平时用的数组的使用会遇到什么问题:

1
2
3
Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

这段代码编译没有问题,运行时会报错ArrayStoreException错误,因为字符串数组不能存放整型元素。在运行时才能发现,很危险。

我们假设一段代码可以编译:

1
2
3
4
Object[] stringLists = new List<String>[10];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
// An ArrayStoreException should be thrown, but the runtime can't detect it.
stringLists[1] = new ArrayList<Integer>();

假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除erasure,即

Object[] stringLists = new List[10];

在编译后会变成

Object[] stringLists = new List[10];

同样,擦除泛型后JVM根本就不会知道源码中new ArrayList<String>()new ArrayList<Integer>()的区别,所以怎么会允许不同泛型的元素最终存到一个固定类型的数组中呢?

注:一切错误尽量在编译的时候发现,如果在线上运行的时候才发现就晚了。所以,由于泛型由于类型擦除,导致有些问题有可能只有在运行时才会被发现,所以从语法的角度直接给做了限定。

上面的例子可以看出数组在编译和运行时是保持类型不变的即reifiable的但是泛型信息将会被擦除,所以泛型是non-reifiable的。

1
2
3
a reifiable type is one whose runtime representation contains same information than its compile-time representa-tion

a non-reifiable type is one whose runtime representation contains less information than its compile-time representa-tion

unchecked warnings

如果使用泛型开发,可能会遇到编译器提示的如下warning:

  • unchecked cast warnings
  • unchecked method invocation warnings
  • unchecked parameterized vararg type warnings
  • unchecked conversion warnings

我们需要竭尽所能消灭掉这些warning,以免运行时发生异常。有以下几点需要注意:

  • 赋值时自动推断

    List arrs = new ArrayList();

后面的Integer是多余的,编译器可以自行推断它的类型。可以直接保留一个<>符号,而不是用ArrayList()

  • 类型推断只对赋值操作有效 类型推断做为参数时是不能编译通过
    1
    2
    3
    4
    5
    6
    
    class Test{
      public static void f(List<Intger> p){}
      public static void main(String[] args){
          f(Lists.newArrayList()); // 不能编译通过
      }
    }
    

无法用泛型实例化

Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过:

1
2
3
4
public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

如果某些场景我们想要需要利用类型参数创建实例,利用反射解决是个不错的方法:

1
2
3
4
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

泛型无法使用instanceof关键字

我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息, 正如我们上面验证过的JVM在运行时期无法识别出ArrayList<Integer>ArrayList<String>的之间的区别:

1
2
3
4
5
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

和上面一样,我们可以使用通配符重新设置bounds(边界)来解决这个问题:

1
2
3
4
5
public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

注意instanceof需要使用reifiable类型,这个包括的内容我们稍后介绍,记住wildcard是其中之一。

Bridge method

It’s a method that allows a class extending a generic class or implementing a generic interface (with a concrete type parameter) to still be used as a raw type.

即桥接方法是一个继承带有泛型的类或实现带有泛型的接口的类中的一个方法,它允许这个类的实例可以通过使用原生参数的方式调用父类的方法。

A synthetic method that the compiler generates in the course of type erasure. It is sometimes needed when a type extends or implements a parameterized class or interface.

它是由编译器在类型擦除的过程中生成的一个「合成」方法。

举个例子:

1
2
3
4
5
public class MyComparator implements Comparator<Integer> {
   public int compare(Integer a, Integer b) {
      //
   }
}

注意,我们并不能用两个Object参数来调用MyComparator实例的compare方法,因为Integer作为方法的参数已经被编译到字节码里, 但是类声明中的Comparator<Integer>中的泛型确实会被擦除。

但是如果只有Integer的方法,那么如何通过父类型的变量(即用接口Comparator声明的变量)去调用对应的方法呢?

答案是编译器会在类型擦除的时候增加一个桥接方法来实现:

1
2
3
4
5
6
7
8
9
10
public class MyComparator implements Comparator<Integer> {
   public int compare(Integer a, Integer b) {
      //
   }

   //THIS is a "bridge method" generated by compiler
   public int compare(Object a, Object b) {
      return compare((Integer)a, (Integer)b);
   }
}

编译器会保证对bridge方法的访问的安全性,用户侧是无法调用者方法的。下面的Object参数调用Comparator的compare方法是编译ok的:

1
2
3
4
5
Object a = 5;
Object b = 6;

Comparator rawComp = new MyComparator();
int comp = rawComp.compare(a, b); // 注意这里是调用的bridge方法

上面的代码使用Comparator类型的变量去调用compare方法,但是实际上是MyComparator实例内的bridge方法,这样是可以成功的, 主要是考虑向前兼容老的非泛型的代码。如果用MyComparator实例去调用该方法则仅仅能够访问Integer参数的同名方法,编译器 会保护对桥接方法的访问。

但是如果使用不能转换为Integer的参数则在运行时会报ClassCastException,因为bridge方法是强制类型转换到当前实例方法类型的。

References

本文首次发布于 LiuShuo’s Blog, 转载请保留原文链接.