Java泛型食用笔记(三) -- 擦除的代价
为了兼容性,Java 采用了擦除来实现泛型,这也付出了一些代价。本节我们讨论擦除带来的问题,我们在使用中应该明白这些问题的原因,避免在这些问题上纠结浪费时间。
1. 不能在静态成员中引用封闭类型参数
在之前的文章中我们也提到过,泛型类型参数的作用范围是类定义的主体部分,但并不包括静态成员。一下代码是非法的
public class Holder<T> {
public static T data; // compile error
public static void set(T data) {} // compile error
}
分析一下,静态方法和静态变量都是类本身的属性,即该类所创建的对象都共享的属性。在我们之前的讨论里知道,Holder<String>
和 Holder<Integer>
其实是一个类,在对象没有创建的时候,我们并不能确定 T 的类型,因此类的泛型类型参数不能作用与静态成员。
如果静态方法需要使用泛型,可以参照我们在第一部分介绍的泛型方法。
2. 泛型的类型不能是基本类型
泛型不能使用基本类型,也就是 ArrayList<int>
是非法的,需要用包装类替换 ArrayList<Integer>
。这也很好理解,我们知道泛型参数编译后都被擦除为 Object,而 Object 是不能存储基本类型 int, double, float
等值的。
3. 无法对类型参数使用 instanceof
由于泛型参数类型在运行时都被擦除为 Object,泛型类型都被擦除为原始类,因此 obj instanceof T
和 obj instanceof ArrayList<String>
, 这种使用方式都会导致编译不通过。
4. 不能直接使用 new 实例化类型参数
不能使用 new T()
这样来实例类型参数对象,一方面是因为泛型参数在运行时都被擦除为 Object,另外一方面,T 的具体类型在运行时才能确定,并不确定他的构造器的参数,故无法使用 new 实例化。同样 new T[5]
这种使用也不能通过编译。
解决方法是采用工厂类,比如说 Class 类
public class GenericTest05<T> {
public T create(Class<T> kind) throws Exception {
return kind.newInstance();
}
}
使用 Class 也有问题,甚至并不推荐,因为 Class.newInstance()
这样获得实例不是对所有类都能成功,比如 Class<Integer>
就会失败。所以建议用通配符限制工厂类型,并显式提供工厂类。
5. 继承泛型接口
因为擦除的原因,你不能同时继承同一个泛型接口的两个变种。比如如下代码:
interface Eat<T> {}
class Fruit implements Eat<Fruit> {}
class Apple extends Fruit implements Eat<Apple> {} // compile error
这种情况其实也并不鲜见,最典型的就是 Comparable<T>
接口。有趣的是,Apple
类如果继承接口 Eat<Fruit>
是合法的,只要他和父类继承的接口精确的相同,包括类型参数也要相同。
6. 方法重载
我们知道,重载方法要求方法的参数类型列表唯一。由于擦除的原因,以下代码是不能编译通过的。
public void f(List<String> list) {}
public void f(List<Integer> list) {}
7. 无法使用泛型数组
Java 中不能创建泛型数组,如下代码编译不通过:
ArrayList<String>[] list = new ArrayList<String>[5]
我们先看一个数组的例子:
Object[] arr = new String[2];
arr[0] = "123"; // compile ok, runtime ok
arr[1] = 123; // compile ok, runtime error ArrayStoreException
String 数组中放入数组显然应该报错,虽然在编译时错误没有发现,好歹运行时把异常抛出。假设泛型数组可以创建,例子变成:
Object[] arr = new ArrayList<String>[2]; // compile error, pretend ok
arr[0] = new ArrayList<String>(); // compile ok, runtime ok
arr[1] = new ArrayList<Integer>(); // compile ok, runtime ok, but actually should throw ArrayStoreException
假设上述程序第一行能编译通过,由于泛型运行时都被擦除成原始类,之后的两行代码都应该能运行成功,明明定义的是 ArrayList<String>
数组,ArrayList<Integer>
也能放入,而且由于类型在运行时擦除,不光编译可以通过,运行时还不会抛出异常,这使得发现问题非常困难。
这种使用场景可以使用容器类代替数组。
小结
本节介绍了泛型的擦除原理带来的一些问题,并尝试阐明这些问题的原因。理解这些问题原因能防止我们在一些泛型力所能及之外的范围浪费时间。