一、对泛型的理解

Java集合有个缺点—把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。

Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

  • 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
  • 由于把对象“丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

从Java 5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如 List<string>,表明该List只能保存字符串类型的对象。</string>

有了泛型以后,程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。

二、泛型擦除

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个 List<string> 类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。</string>

上述规则即为泛型擦除,可以通过下面代码进一步理解泛型擦除:

List<String> list1 = ...;
List list2 = list1; // list2将元素当做Object处理

从逻辑上来看,List<string> 是List的子类,如果直接把一个List对象赋给一个List<string>对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List对象赋给一个 List<string> 对象,编译器仅仅提示“未经检查的转换”。</string></string></string>

上述规则叫做泛型转换,可以通过下面代码进一步理解泛型转换:

List list1 = ...;
List<String> list2 = list1; // 编译时警告“未经检查的转换”

三、List<? super T>和List<? extends T>的区别

  • ? 是类型通配符,List<?> 可以表示各种泛型List的父类,意思是元素类型未知的List;
  • List<? super T> 用于设定类型通配符的下限,此处 ? 代表一个未知的类型,但它必须是T的父类型;
  • List<? extends T> 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型。

在Java的早期设计中,允许把Integer[]数组赋值给Number[]变量,此时如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但在运行时抛出ArrayStoreException异常。这显然是一种不安全的设计,因此Java在泛型设计时进行了改进,它不再允许把 List<integer> 对象赋值给 List<number> 变量。</number></integer>

数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但G<foo> 不是 G<bar> 的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变,也就是说,Java的数组支持型变,但Java集合并不支持型变。Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。</bar></foo>