文章目录
- 说一说你对泛型的理解
- 介绍一下泛型擦除
- List<? super T>和List<? extends T>有什么区别?
说一说你对泛型的理解
Java集合有个缺点—把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。
Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:
-
集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
-
由于把对象“丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。
从Java 5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如 List< String >,表明该List只能保存字符串类型的对象。
有了泛型以后,程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。
面试场景:
面试官:说说你对泛型的理解。
我:好的。我们知道,在我们使用集合的时候,集合默认是允许我们放入一个Object类型的对象的。而Java中所有的类都默认继承了Object类。因此其实所有的类都可以放入到集合中。
我:这就造成了一个问题,如果我们想要一个只能存放String类型的集合,那么可能我们放入String之后,又放入了Integer等类型。
我:并且我们在取出元素的时候,它的返回类型也是Object,那么此时我们就得显式的进行类型转换,提高了ClassCastException的可能性。
我:所以在JDK1.5之后,Java提供了一种“参数化类型的概念”,也就是我们说的泛型,他允许我们在使用集合的时候设定类型,而之后再向这个集合中添加或获取元素的时候,元素类型必须是这个类型的或者其子类。这样子我们就能自己控制集合中元素的类型了,也就无需在对元素进行强制类型转换。
我:以上是我对泛型的理解。
介绍一下泛型擦除
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个
List < String > 类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。
上述规则即为泛型擦除,可以通过下面代码进一步理解泛型擦除:
List<String> list1 = ...; List list2 = list1; // list2将元素当做Object处理
从逻辑上来看,List< String > 是List的子类,如果直接把一个List对象赋给一个List< String >对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List对象赋给一个 List< 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 > 变量。
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但G< Foo > 不是 G< Bar > 的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变,也就是说,Java的数组支持型变,但Java集合并不支持型变Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。