0%

Java 泛型、上下界通配符、泛型擦除

泛型解决的是什么问题?

在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。

通配符表示什么?

通配符?表示未知类型

泛型存在什么问题?

泛型的类型存在继承关系,不代表使用泛型的类也存在继承关系。

例如Integer是Number的子类,List不是List的子类,不能把List的变量赋值给List的变量。

为什么不能把List的变量赋值给List的变量?

因为List可能还存储Float、Double等类型的元素,List如果可以赋值给List,List里就可以存储Float、Double等非Integer元素了,这与泛型的定义冲突

上界通配符是什么?

<? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”。

上界通配符只能读取,因为读取到的类型进行类型转换是安全的,所有的子类都可以安全的转换为父类。

但是不能插入,因为不知道具体类型。

下界通配符是什么?

<? super T>:是指 “下界通配符(Lower Bounds Wildcards)”

当你知道集合里所持有的元素类型都是A及其父类的时候,此时往list集合里面插入A及其子类(B或C)是安全的。

泛型通配符具体的使用场景?

有一组类继承关系如下:

一个常见的获取容器元素的需求就是,写一个基于容器元素基类的方法,从容器中去取出元素进行打印,而不用针对每一个具体子类定义打印方法。

但实际List<Apple>不能赋值给List<Fruit>,编译会报错,如下:

因为List<Fruit>可能还持有Banana或Pear,与List<Apple>不能等价。

List<Fruit>改为List<? extends Fruit>就可以了,这个上界通配符的意思是,List里持有的都是Fruit的子类,具体是哪些子类不知道,List<Apple>可以看做是List<? extends Fruit>子类,从List<? extends Fruit>也只能取出Fruit,不能取出Fruit的子类。但是不能给List<? extends Fruit>里add任何的Apple、Banana、Pear,因为 ? extends Fruit 并不知道具体的子类是什么。

需要读取就用上界匹配符。

当需要往容器添加元素,List<Object>不能直接赋值给List<Fruit>变量。

因为List<Object>可能存在非Fruit的元素,如果允许赋值,跟类型定义就矛盾了。

解决方法就是把List<Fruit>改为List<? super Fruit>

addVariousFruits(List<? super Fruit> fruitList)

下界通配符表示容器中都是Fruit的父类元素,List<? super Fruit>List<Object>的父类,所以List<Object>可以赋值给List<? super Fruit>变量。

什么时候用上界通配符?什么时候用下界通配符?

PECS原则:Producer extends Consumer super

  • 如果你只需要从集合中获得类型T , 使用<? extends T>通配符
  • 如果你只需要将类型T放到集合中, 使用<? super T>通配符
  • 如果你既要获取又要放置元素,则不使用任何通配符。

泛型擦除是什么意思?

仅于编译时类型检查,在运行时擦除类型信息。

生成的字节码中不包含泛型的类型信息。

泛型擦除会带来什么问题?

在编译时不一定能靠编译器坚持出类型错误,擦除使得使用泛型的代码可以和没有使用泛型的代码随意互用,这样运行时有可能发生类型转换异常。

比如通过反射调用代码,可以绕过编译器的类型错误检查,因为反射是在运行时发生的,编译后泛型的类型就被擦除了,所以不会报错。


深入理解Java虚拟机(第2版)10.3.1 泛型与类型擦除

如下的代码输出结果为true

1
2
3
List<String> l1 = new ArrayList<String>(); 
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());

既然泛型擦除有问题,为什么当初不设计成跟C++模板那种一样呢?

C++的模板实例化类时会为每种类型都创建新的类,就会造成代码膨胀。

同时为了保持JDK5之前的代码能够兼容运行。

泛型擦除的优点

  1. 兼容JDK5之前的代码
  2. 不会像C++模板造成代码膨胀

泛型擦除的缺点

  1. 编译时不一定能靠编译器坚持出类型错误,这样运行时有可能发生类型转换异常,例如反射可以绕过编译器的类型错误检查。
  2. 不能创建具体类型的泛型数组。
  3. 方法重载时不同的泛型具体类会被认为擦除后是一个类型从而禁止重载。

为什么不能创建具体类型的泛型数组?

List<Integer>[] list = new ArrayList<Integer>[];

这段代码编译会不通过

List<Integer>List<Boolean>在 jvm 中等同于List<Object>,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer>类型还是 List<Boolean>类型。