1、基础知识
泛型是 JDK5 引入的参数化类型特性,所谓参数化类型就是把类型当成参数一样传递,它使得一个类型在定义类、接口和方法时成为一个参数,类似于方法声明中的形式参数,不同之处在于形式参数的输入是值,而类型形参的输入是类型。
为什么要使用泛型
与非泛型代码相比,使用泛型的代码具有许多优点:
-
使代码更健壮。Java 编译器会对泛型代码进行强类型检查,如果代码违反类型安全,则会编译报错。修复编译时错误比修复运行时错误容易,后者可能很难找到。将运行时潜在的类型转换异常提前到编译时检查,也是泛型的重要作用之一。只要编译期没有警告,运行期就不会出现 ClassCastException
-
使代码更简洁。泛型消除了强制类型转换,非泛型代码段需要强制转换:
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);
使用泛型重写时,代码不需要强制转换:
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast
-
使代码更灵活。通过使用泛型,程序员可以实现泛型算法,这些算法可以处理不同类型的集合(增强了代码复用性),可以自定义,并且类型安全且易于阅读
泛型相关术语
Plate<T>
中的 T 被称为类型参数,而Plate<T>
整个被称为泛型类型,Plate
则称为原始类型Plate<Banana>
中的 Banana 被称为实际类型参数,而Plate<Banana>
整个被称为参数化的类型(Parameterized Type)
基础使用
泛型可以定义在类、接口和方法上:
// 泛型接口
public interface Box<T> {
public void set(T t);
public T get();
}
// 泛型类,不知道 BoxImpl 具体的泛型类型
public BoxImpl<T> implements Box<T> {
public void set(T t);
public T get();
}
// 泛型类,知道 BoxImpl 具体的泛型类型是 Apple
public BoxImpl implements Plate<Apple> {
public void set(Apple apple);
public Apple get();
}
// 泛型方法,泛型方法上的类型参数范围仅限于声明它的方法,
// 方法上的类型参数列表要位于返回类型之前
public <T> BoxImpl<T> getBox() {
return new BoxImpl<>();
}
需要注意类上声明的泛型只对非静态成员有效,但是泛型方法可以是一个静态方法。
此外官方对于泛型的类型形参有命名规范:
- E - Element (Java 集合框架广泛使用)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
可以对类型参数进行限定,限定主要有两个作用:
- 限制可用于实例化泛型类型的类型
- 限定类型参数允许你调用在范围中定义的方法
比如说:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
// 参数 U 必须继承自 Number,输入字符串会报错,体现第 1 点
integerBox.inspect("some text"); // error: this is still String!
}
}
--------------------------------------------------------------------------------
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) {
this.n = n;
}
public boolean isEven() {
// n 的类型被限定为继承 Integer,所以它可以使用 Integer 的方法,体现第 2 点
return n.intValue() % 2 == 0;
}
// ...
}
如果一个类型参数具有多个限定:
<T extends B1 & B2 & B3>
且 B1、B2、B3 中有一个是类的话,那么它必须居于首位,否则会编译报错。此外,为了提高效率,应该将标签接口(即没有方法的接口)放在边界列表的末尾,如 Serializable。比如说一个泛型类 Data<T extends Serializable & Comparable>
,发生类型擦除至原始类型后,会用 Serializable 替换 T,使得编译器在必要时会向 Comparable 插入强制类型转换。
类型推断
类型推断是 Java 编译器查看每个方法调用和相应声明,以使用适用的类型参数的能力。推断算法确定参数的类型,以及确定结果是否被分配或返回的类型(如果有)。最后,推断算法尝试找到与所有参数一起使用的最具体的类型。比如说,在下面的示例中,会推断传递给 pick 方法的第二个参数的类型为 Serializable:
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
泛型方法也引入了类型推断,可以像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。
从 Java SE 7 开始,只要编译器可以从上下文确定或推断出类型参数,就可以用一组空的类型参数( <> )替换调用泛型类的构造函数所需的类型参数:
// Java7 开始 <> 内的类型在可以类型推断的情况下可以省略
Box<Integer> integerBox = new Box<>();
// Java7 之前调用构造方法时 <> 内的类型不能省略
Box<Integer> integerBox = new Box<Integer>();
2、通配符与泛型限定
实际使用泛型时,可能会出现这种情形:编写一种适用于 List<Number>
、List<Integer>
和 List<Double>
的方法。总不能以每一种类型作为形参写出三个方法吧?如果想要用一个方法解决,这时就需要用到泛型限定了。
泛型限定依赖于通配符 ?,<? extends A>
是泛型上限,表示该类型是 A 本身或其子类型,<? super A>
是泛型下限,表示该类型是 A 本身或其父类,而 <?>
则为不受限的通配符。
泛型上下限
上面说到的编写适用于 List<Number>
、List<Integer>
和 List<Double>
的方法,就可以用泛型上限来实现。比如说要计算 List 元素中所有数据之和:
private double addToSum(List<? extends Number> list) {
double sum = 0.0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
使用 List<? extends Number>
类型作为形参可以接收 List<Number>
、List<Integer>
和 List<Double>
等多种类型。这就体现出泛型上限的优点,即扩大了兼容的范围,使得方法形参的匹配范围从单一的 List<Number>
扩展到泛型类型为 Number 本身及其子类的 List。
但是这样做也有两个缺点:
- 写:不能再向
List<? extends Number>
中添加新元素或修改已经存在的元素了。但是添加 null,或者通过反射添加/修改元素是可以的,只是没法保证类型安全,可能会出现运行时类型转换异常。 - 读:从
List<? extends Number>
中取元素时,该元素只能被转成 Object 或者上限类型 Number。原因是编译器只知道容器内装着 Number 或者它的派生类对象,但是并不知道被取出元素的具体类型,可能是 Integer,也可能是 Float 或 Double。所以在使用限定泛型List<? extends Number>
之后,它的类型标签不是某个具体的类型,而是一个占位符 CAP#1,来表示捕获一个 Number 或 Number 的子类,具体是什么类型,不知道……这也解释了上一条中为什么不能向使用了上限的集合中添加元素的原因,因为编译器不知道你要添加的元素类型是否与占位符 CAP#1 匹配,所以干脆就都不允许添加了。
List<? extends XXX>
通常被非正式的认为是只读的,比如:
class NaturalNumber {
private int i;
public NaturalNumber(int i) { this.i = i; }
// ...
}
class EvenNumber extends NaturalNumber {
public EvenNumber(int i) { super(i); }
// ...
}
你可以将 List<EvenNumber>
类型的对象赋值给 List<? extends NaturalNumber>
,但是不能调用后者的 add() 添加元素:
List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // compile-time error
因为 List<EvenNumber>
是 List<? extends NaturalNumber>
的一个子类型,所以可以将 le 赋给 ln,但是不能用 ln 将自然数添加到偶数列表中,它可以对该列表做以下操作:
- 可以添加 null
- 可以调用 clear
- 可以获取迭代器( iterator )和调用 remove
- 可以捕获通配符和写入从列表中读取的元素
所以 List<? extends Xxx>
并不是严格意义上的只读,它是不能在列表中存储一个新的元素或改变一个现有的元素。
类似地,泛型下限也是有副作用的:
public void test() {
Box<? super Integer> box = new Box<Integer>();
// 可以
box.set(new Integer());
// 可以
box.set(new Number());
// 不可以,编译报错,box 可以存数据,但是取数据时泛型信息丢失了,只能用 Object 存放
Integer num1 = box.get();
// 可以,泛型下限取出的数据只能存成 Object 类型
Object num2 = box.get();
}
无界通配符
出现在泛型上限如 List<? extends XXX>
和泛型下限如 List<? super XXX>
中的 ? 分别被称为上界通配符和下界通配符。而单个的 ? 如 List<?>
被称为无界通配符,该泛型类等价于 List<? extends Object>
。
使用无界通配符的泛型既不能读也不能写(只能插入 null),它用来进行类型安全检查:
- List 不会进行类型安全检查。
List<?>
会进行类型安全检查。
有两种情况,无界通配符是一种有用的方法:
- 如果你正在编写可以使用 Object 类中提供的功能来实现的方法。
- 当代码使用泛型类中不依赖于类型形参的方法时。例如,List.size() 或 List.clear()。实际上,
Class<?>
经常被使用,因为Class<T>
中的大多数方法都不依赖于 T。
比如说你想编写方法 printList 打印来打印任何类型的列表:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
如果像上面那样使用 List<Object>
作为形参类型就无法实现这个目标,因为它只能接收 List<Object>
,无法接收其他类型的列表,因为它们不是 List<Object>
的子类型。只有把 List<Object>
改为 List<?>
才可以,因为对于任何具体类型 T,List<T>
都是 List<?>
的子类型:
通配符使用准则
为了便于讨论,将变量视为提供以下两个功能之一将很有帮助:
- "输入"变量:输入变量将数据提供给代码。想象一个具有两个参数的复制方法: copy(src, dest) 。src 参数提供要复制的数据,因此它是输入参数。
- "输出"变量:输出变量保存要在其他地方使用的数据。在复制示例 copy(src, dest) 中,dest 参数接受数据,因此它是输出参数。
当然,某些变量既用于“输入”又用于“输出”目的(准则中也解决了这种情况)。准则如下:
- 使用上限通配符定义输入变量,使用 extends 关键字。
- 使用下限通配符定义输出变量,使用 super 关键字。
- 如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
- 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。
这些准则不适用于方法的返回类型。应该避免使用通配符作为返回类型,因为这会迫使程序员使用代码来处理通配符。
与上述观点类似的是泛型的 PECS 原则:Producer Extends Consumer Super,站在 List 的角度,从 List 中取元素的是生产者 Producer,只读不写时应该用 Extends 限定;而向 List 中写入元素的是消费者 Consumer,写入时应该用 Super 限定。典型用例是 Collections 中的 copy 方法:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size()) {
throw new IndexOutOfBoundsException("Source does not fit in dest");
} else {
if (srcSize < 10 || src instanceof RandomAccess && dest instanceof RandomAccess) {
for (int i = 0; i < srcSize; ++i) {
dest.set(i, src.get(i));
}
} else {
ListIterator di = dest.listIterator();
ListIterator si = src.listIterator();
for (int i = 0; i < srcSize; ++i) {
di.next();
di.set(si.next());
}
}
}
}
通配符捕获
Collections 中的 swap() 能体现出 List<?>
的作用:
public static void swap(@NotNull List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
Collections 的源码就是这么写的,由于前边说到过 List<?>
这种无限定通配符既不能读也不能写,因此我们自己在写 list.get(i) 时其实是编译报错的:
不兼容的类型: Object无法转换为CAP#1
var0.set(var1, var0.set(var2, var0.get(var1)));
^
其中, CAP#1是新类型变量:
CAP#1从?的捕获扩展Object
根据官方文档指引,可以通过编写捕获通配符的私有帮助器来修复它:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
public static <T> void swapHelper(List<T> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
这种方法其实是通过参数传递把 List<?>
传递给了 swapHelper() 的参数 List<T>,做了一步类型推断从而避免了编译错误。
各种泛型形式的区分
最后我们对泛型中的各种形式进行区分:
Plate
原始类型,不做类型安全检查Plate<Object>
可读可写,可存任何类型数据Plate<?>
不可读写(只能插入 null),唯一作用是做类型检查,相当于Plate<? extends Object>
Plate<T>
泛型类型,可读可写Plate<? extends T>
泛型上限,可读不可写(但可以写入 null 或者通过反射写入任何类型的值)Plate<? super T>
泛型下限,可写入运行时类型及其子类类型元素,作为方法参数赋值时可以指向 T 及其父类型
3、泛型中的赋值问题
泛型的继承和子类型
只要类型兼容就可以将一种类型的的对象分配给另一种类型的对象。例如,你可以将一个 Integer 分配给一个 Object ,因为 Object 是 Integer 的超类型之一:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
由于 Integer “is a” Object,所以允许这样分配。泛型也是如此,你可以执行泛型类型调用,将 Number 作为其类型参数传递,并且如果该参数与 Number 兼容,则可以随后进行 add 的任何后续调用:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
但是有一种情况要注意:
public void boxTest(Box<Number> n) { /* ... */ }
此时你不能在调用该方法时传入一个 Box<Integer>
作为参数,因为 Integer 继承自 Number,但是 Box<Integer>
与 Box<Number>
之间没有继承关系,它们的共同父类是 Object。类似的,List<Number>
与 List<Integer>
之间也没有继承关系,二者的公共父类是 List<?>
。再延伸一步,由于 List<? extends Integer>
是 List<? extends Number>
的子类型,所以前者可以赋值给后者:
记住判断两个对象之间能否赋值,就看这两个对象的类型是否兼容,对于泛型类和非泛型类都是如此。
在泛型中,除了上图中表示的限定类型相关的继承关系之外,还有一种继承关系,假设有关系 ColorPlate -> BigPlate -> Plate(-> 表示继承),那么对于同一个参数类型 T,以上继承关系仍然成立,即 ColorPlate<T> -> BigPlate<T> -> Plate<T>。
原始类型
原始类型(Raw Types)是没有任何类型参数的泛型类或接口的名称。例如,给定通用 Box 类:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
正常创建 Box 的参数化类型需要为 T 指定一个实际的类型参数,如
Box<Integer> intBox = new Box<>();
但如果省略类型参数,创建的就是 Box 的原始类型:
Box rawBox = new Box();
因此 Box 是通用类型 Box<T>
的原始类型,但是需要注意非泛型类或接口不是原始类型(类上没有类型参数的不算原始类型)。JDK5 之前,诸如 Collections 类的许多 API 类不是通用的,使用原始类型时实际上会获得泛型行为(指 Box 为你提供对象)。为了向后兼容,允许将参数化类型分配给其原始类型:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
但是反之则会收到警告,该警告表明原始类型会绕过通用类型检查,从而将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型:
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
因为你不知道 rawBox 中的元素是否都是 Integer 类型的……类似的,如果你使用原始类型来调用在相应的泛型类型中定义的泛型方法,也会收到警告:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
总之,无论是将原始类型给泛型类型,还是将泛型类型给原始类型,都会破坏类型安全,有发生运行时异常的可能:
// 情况一:
public static void testGeneric() {
List rawList = new ArrayList();
rawList.add(1);
// Unchecked assignment: 'java.util.List' to 'java.util.List<java.lang.String>'
List<String> stringList = rawList; // 这是有问题的,但是编译没报错
String string = stringList.get(0); // 这里类型转换的时候抛异常了
}
// 情况二:
public static void testGeneric() {
List<String> stringList = new ArrayList<>();
stringList.add("1");
List rawList = stringList;
Object object = rawList.get(0); // 转换成 Object 可以
Integer integer = rawList.get(0); // 转换成其他类型编译时就会报错
}
上面示例代码的情况一会引发堆污染。堆污染发生于泛型类型的变量引用的是不属于该泛型类型的对象时。如果程序执行某些操作,在编译时产生未经检查的警告,就会出现这种情况。如果在编译时(在编译时类型检查规则的限制内)或在运行时,涉及参数化类型的操作(例如,类型转换或方法调用)的正确性无法验证时,则会生成 unchecked warning (未经检查的警告)。例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。
在正常情况下,当所有代码同时编译时,编译器会生成未经检查的警告,以引起你对潜在堆污染的注意。如果单独编译代码的各个部分,则很难检测到堆污染的潜在风险。如果确保代码在没有警告的情况下编译,则不会发生堆污染。
如果产生了 unchecked warning,可以使用 @SuppressWarnings(“unchecked”) 注解抑制该警告。
4、类型擦除
Java 的泛型是一种“伪泛型”,它是在编译期实现的,并不会在虚拟机层面产生新的类。比如 List<Number>.getClass() == List<Integer>.getClass()
这个表达式的值为 true,二者得到的都是 List.class。Java 编译器是通过泛型擦除实现这一点的。
泛型擦除的具体内容:
- 若泛型为无限定的类型,如 T、E,则替换为 Object
- 若泛型为有限定的类型,如
<T extends A & B & C>
,则替换为第一个限定类型,在这里就是 A - 必要时插入类型转换,以保持类型安全
- 生成桥接方法以在扩展的泛型类型中保留多态
类型擦除可确保不会为参数化类型创建新的类,产生的字节码仅包含普通的类,接口和方法。因此,泛型不会产生运行时开销。
考虑泛型的时候,不能以运行时的思维去想,虚拟机中没有泛型,只在编译阶段,编译器才考虑泛型。
比如说泛型无限定时:
public class Plate<T> {
private T data;
}
data 的类型直接被擦除为 Object:
public class com/frank/generic/Plate {
// compiled from: Plate.java
private Ljava/lang/Object; data
// ...
}
而当泛型有限定时:
public class Plate<T extends Comparable<T> & Serializable> {
private T data;
}
会被替换为第一种限定类型 Comparable:
public class com/frank/generic/Plate {
// compiled from: Plate.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/lang/Comparable; data
// ...
}
当限定中存在多种类型时,如果既有类又有接口,那么类一定要放在接口之前。
桥方法
以上的擦除处理会在多态时产生问题,为了解决这个问题,编译器使用了桥方法。
一个简单的场景是,需要实现一个带有泛型的接口,其源代码和字节码如下:
public interface Play<T> {
void set(T t);
}
// Play 对应的字节码
public abstract interface com/frank/generic/Play {
public abstract set(Ljava/lang/Object;)V
}
实现类 Person 的源码和字节码:
public class Person implements Play<String> {
private String data;
@Override
public void set(String data) {
this.data = data;
}
}
// 字节码
public class com/frank/generic/Person implements com/frank/generic/Play {
// compiled from: Person.java
// access flags 0x2
private Ljava/lang/String; data
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/frank/generic/Person; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public set(Ljava/lang/String;)V
L0
LINENUMBER 9 L0
ALOAD 0
ALOAD 1
PUTFIELD com/frank/generic/Person.data : Ljava/lang/String;
L1
LINENUMBER 10 L1
RETURN
L2
LOCALVARIABLE this Lcom/frank/generic/Person; L0 L2 0
LOCALVARIABLE data Ljava/lang/String; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// 编译器生成的桥方法
// access flags 0x1041
public synthetic bridge set(Ljava/lang/Object;)V
L0
LINENUMBER 3 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/String
INVOKEVIRTUAL com/frank/generic/Person.set (Ljava/lang/String;)V
RETURN
L1
LOCALVARIABLE this Lcom/frank/generic/Person; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
}
Play 接口中的 set() 中的泛型被擦除为 Object,而 Person 中的 set() 的参数类型仍为 String,也就是说泛型擦除后,Person 并没有实现 Play 中的 set() 方法。因此编译器自动生成了一个桥方法 public synthetic bridge set(Ljava/lang/Object;),以此实现 Play 接口中的 set():
// Play 中的 set() 擦除后:
void set(Object object);
// Person 中的 set():
public void set(String data) {
this.data = data;
}
// Person 中的桥方法:
public void set(Object object) {
set((String)object);
}
泛型与反射
既然泛型在编译阶段被擦除,字节码中没有泛型信息,那为什么反射能拿到相关信息呢?
public class Test {
private Map<String, Integer> map = new HashMap<>();
public static void main(String[] args) throws NoSuchFieldException {
Field field = Test.class.getDeclaredField("map");
System.out.println(field.getGenericType()); // java.util.Map<java.lang.String, java.lang.Integer>
if (field.getGenericType() instanceof ParameterizedType) {
ParameterizedType genericType = (ParameterizedType) field.getGenericType();
System.out.println(genericType.getRawType()); // interface java.util.Map
for (Type type : genericType.getActualTypeArguments()) {
System.out.println(type); // class java.lang.String,class java.lang.Integer
}
System.out.println(genericType.getOwnerType()); // null
}
}
}
这是因为泛型信息虽然被擦除不在字节码中,但是被保存在类的常量池里。Retrofit 正是利用了这一点才实现了通过 Call<XXX>
生成一个 XXX 实例的。
为了表达泛型类型声明,使用 java.lang.reflect 包中提供的接口 Type,该接口包含了一个实现类 Class 和四个实现接口,他们分别是:
- TypeVariable 泛型类型变量。可以获取泛型上下限等信息,如
T extends Comparable\<? super T>
- ParameterizedType 具体的泛型类型,可以获得元数据中泛型签名类型(泛型真实类型),如
Comparable<? super T>
- GenericArrayType 当需要描述的类型是泛型类的数组时,比如 T[](List[]、Map[]),此接口会作为 Type 的实现
- WildcardType 通配符泛型,获得上下限信息,如 ? super T
泛型的约束与局限性
这些约束与局限性大多是因为类型擦除造成的。
泛型类型变量不能使用基本类型
比如没有 ArrayList<int>
,只有 ArrayList<Integer>
。这是因为当类型擦除后,ArrayList 的原始类中的类型变量 T 被替换成 Object,但 Object 类型不能存放 int 值。
不能使用 instanceof 运算符
因为擦除后,ArrayList<String>
只剩下原始类型,泛型信息 String 不存在了,所有没法使用 instanceof:
ArrayList<String> strings = new ArrayList<>();
// 不可以,因为 String 会被擦除掉
if (strings instanceof ArrayList<String>)
// 可以
if (strings instanceof ArrayList<?>)
不能使用泛型修饰静态成员
泛型类中的类型参数的实例化是在定义泛型类型对象(比如 ArrayList<Integer>
)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么。因此,如下形式是不可以使用的:
class Test<T> {
// 类型参数不可以修饰静态变量
public static T data;
// 类型参数不可以修饰静态方法
public static T test(T t) {...}
// 可以,这是泛型方法,这个方法上的 T 不是泛型类上的 T,将这里的 T 换成其他字母也是一样
public static <T> T get(T t) {
return t;
}
}
但是类型参数可以出现在泛型方法上。
可能会导致泛型类中的方法冲突
// 类型 T 擦除后会变成 Object,与下一个方法签名相同发生冲突
@Override
public boolean equals(T t) {
return super equals(t);
}
@Override
public boolean equals(Object obj) {
return super equals(obj);
}
无法创建泛型实例
确切的说是无法通过构造方法创建一个泛型类型的实例,但是通过反射是可以的:
public static <T> void test(List<T> list, Class<T> cls) {
// 通过构造方法创建对象不可以,编译报错
T element = new T();
// 通过反射调用创建对象是可以的
T element1 = cls.newInstance();
list.add(element1);
}
没有泛型数组
Fruit 是 Apple 的父类,那么 Fruit[] 就是 Apple[] 的父类,这个现象就是数组的协变。假如我们允许泛型数组,Plate<Fruit>[]
和 Plate<Apple>[]
,类型擦除后没办法满足数组协变的原则,因此 Java 中没有泛型数组。
无法创建,捕获或抛出参数化类型的对象
泛型类不能直接或间接地继承 Throwable 类,例如如下代码会编译报错:
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
泛型方法不能捕获类型形参的实例:
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
但是可以在 throws 子句中使用类型形参:
class Parser<T extends Exception> {
public void parse(File file) throws T { // OK
// ...
}
}
更多细节可以参考《Java 核心技术卷一》8.6 节“约束与局限性”。
不可具体化类型及其潜在漏洞
具体化类型是其类型信息在运行时完全可用的类型。这包括基本类型,非泛型类型,原始类型和无界通配符的调用。
不可具体化的类型是在编译时通过类型擦除移除信息的类型 - 未定义为无界通配符的泛型类型的调用。不可具体化的类型在运行时没有提供所有信息,该类型的示例是 List<String>
和 List<Number>
,JVM 无法在运行时区分这些类型。在某些情况下,不能使用不可具体化类型:例如,在 instanceof
表达式中,或作为数组中的元素。
具有不可具体化的形式参数的 Varargs 方法的潜在漏洞,包含 vararg 输入参数的泛型方法可能会导致堆污染。比如:
public class ArrayBuilder {
public static <T> void addToList (List<T> listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}
public static void faultyMethod(List<String>... l) {
Object[] objectArray = l; // Valid
objectArray[0] = Arrays.asList(42);
String s = l[0].get(0); // ClassCastException thrown here
}
}
编译时 addToList() 的定义会产生警告:
warning: [varargs] Possible heap pollution from parameterized vararg type T
这是因为,当编译器遇到 varargs 方法时,它会将 varargs 形式参数转换为数组。但是,Java 编程语言不允许创建参数化类型的数组。对于参数 T… elements,会先转换成 T[] elements,经过类型擦除后,变为 Object[] elements,所以才可以直接像 Object[] objectArray = l
这样赋值,但是这样可能会引入堆污染……
5、思考题
考虑如下代码的返回值为何必须强转为 T:
public <T extends ViewGroup> T get() { //CAP#1 标记匹配,匹配不上就会编译报错
return (T)new LinearLayout();
}