Java 中的泛型
Java 中的泛型实现了参数类型化的概念。
主要有以下形式:
class OneClazz<T>{
T t;
<Y> void fun(){}
}
本文主要记录 Java 泛型一些比较特殊的知识点。
泛型特性
泛型在 Java SE5 被引入,可以在类和方法中,将类型作为类型参数传入。
泛型类型参数会在实际运行时被擦除到他的第一个边界。如<T>
会被擦除为Objet
,而<T extends ClazzA>
则会被擦除为ClazzA
。
需要注意的地方
不能有泛型数组
这是因为 Java 中 Object[]默认为所有数组的父类,如下代码虽然在编译期不会报错,但是在运行时会被检查出 objArr 指向的数组实际类型(String)和要赋予的类型(Integer)不一致而报错。
也就是说,数组只能存放定义的实际类型以及他们的子类型。
Object[] objArr = new String[10];
objArr[0] = 1;
但是,如果支持泛型数组:由于泛型类型参数会在运行时被擦除,导致即使到了运行时也无法发现这个错误,从而会导致错误。
如下,加入支持泛型参数,则 objArr1 中实际保存的类型(Map<String,Integer>),在编译的时候由于 objArr1 和 objArr2 都是 Object 类型的数组,编译通过;在运行的时候,由于 Map 中的泛型参数类型已经被擦除,也无法区分 objArr1 和 objArr2 中实际指向的两个 Map<K,V>数组,也是合法的,这样原本定义的是 Map<String,Integer>数组,却可以保存任何类似的 Map,而这本来是不允许的。
Object[] objArr1 = new Map<String,Integer>[10];
Object[] objArr2 = new Map<Double,Integer>[10];
objArr1[0] = objArr2[0];
Collections 类通过一种别扭的方法绕过了这个问题,在 Collections 类编译时会产生类型未检查转换的警告。
ArrayList
具体实现的构造函数如下:class ArrayList<V>{ private V[] backingArray; public ArrayList(){ backingArray = (V[])new Object()[DEFAULT_SIZE]; } }
为何这些代码在访问
backingArray
时没有产生ArrayStoreException
呢?无论如何,都不能将Object
数组赋给String
数组。因为泛型是通过擦除实现的,backingArray
的类型实际上就是Object[]
,因为Object
代替了V
。这意味着:实际上这个类期望
backingArray
是一个Object
数组,但是编译器要进行额外的类型检查,以确保它包含V
类型的对象。来源:https://www.ibm.com/developerworks/cn/java/j-jtp01255.html
泛型容器
由于泛型的类型在运行时会被擦除,所以将类型检查放到了编译期。
List<Clazz>
泛型列表只能保存指定泛型类型T
的数据,而不能保存其子类。
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
//编译时报错,类型不兼容
List<Fruit> fruits = new ArrayList<Apple>();
但是能保存 Fruit 的容器应该也要能安全的保存 Apple,为了实现这一点,类似于数组中Object[] arr = Apple[]
的向上转型,可以使用?
引入协变。
协变
List<? extends T>
可以合法的指向一个List< SubT>
,这个过程会完成自动向上转型,成为可以持有某个诸如 T 或者 T 的子类的 List,但是编译器不知道这个类具体是什么,所以拒绝向其中传递任何类型对象,即使 Object 也不行。
可以这么想,<? extends T>
表示的是 T 的子类,那么List<? extends T>
保存的便是T 的某个子类,所以不能保存 Object 或者 T 等类型,又由于列表不能保存不同的类型,所以也不能保存任何 T 的子类,即容器将数组在运行时才会有的类型检查放到了编译期(原因是运行时类型会被擦除)。
List<? extends Fruit> fruits = new ArrayList<Apple>();//可以安全的应用
fruits2.add(new Apple());//编译时报错,类型转化错误
fruits2.add(new Fruit());//编译时报错
逆变
List<? super T>
主动声明通配符?
的超类型为T
,即 List 保存的是T 的某个父类,那么 List 也可以安全的保存T 或者 T 的子类。
void fun(List<? super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit());//error 类型错误
}
参考资料
java 为什么不支持泛型数组? - ylxfc 的回答 - 知乎
Java 理论和实践 - 了解泛型 - 识别和避免学习使用泛型过程中的陷阱
《Java 编程思想 第 4 版》