因为类型擦除,使用 Java 泛型时需要考虑一些限制
不能使用基本类型实例化类型参数
不能用类型参数代替基本类型。因此,没有PairObject
类型无法持有基本类型。
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
Pair<String> p = (Pair<String>) a; // Warning -- can only test that a is a Pair
这时因为类型擦除过后,所有的泛型都是同一个 class ,所以上述比较和转换没有意义,故会报错。
可以使用 getClass()
来返回原始类型:
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employee.getClass()) // they are equal
两次调用的 getCalss()
都将返回 Pair.class 。
不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Pair<String>[] table = new Pair<String>[10]; // Error
擦除之后,table 的类型是 Pair[]。可以把它转换为 Object[]:
Object[] objarray = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个 Array-StoreException 异常:
objarray[0] = "Hello"; // Error -- component type is Pair
不过对于泛型类型,擦除会使这种机制无效。以下赋值:
objarray[0] = new Pair<Employee>();
能够通过数组存储检查,不过仍会导致一个类型错误。
要说明的是,只是不允许创建这些数组,而声明类型为
Pair\<String>[]
的变量仍是合法的。不过不能用new Pair\<String>[10]
初始化这个变量。
要使用泛型数组,必须通过强制转型思想带泛型的数组:
Pair<String>[] table = (Pair<String>[]) new Pair[2]; // Warning
// 通配符类型的数组
Pair<String>[] table = (Pair<String>[]) new Pair<?>[2];
结果将是不安全的。如果在 table[0] 中存储一个 Pair
如果需要收集参数化类型对象,只有一种安全而有效的方法:使用 ArrayList:ArrayList
Varargs 警告
向参数个数可变的方法传递一个泛型类型的实例。比如一个可变的参数方法:
public static <T> addAll(Clooection<T> coll, T...ts) {
for (t : ts) coll.add(t);
}
参数 ts 是一个数组,包含提供的所有实参。可以这样来调用:
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
为了调用这个方法,Java虚拟机必须建立一个Pair
可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解 @SuppressWarnings("unchecked")
。或者在 Java SE 7 中,还可以用 @SafeVarargs
直接标注 addAll 方法:
不能实例化类型变量
不能使用像 new T(...)
,new T[…] 或 T.class 这样的表达式中的类型变量。例如,下面的 Pair
public Pair() {
first = new T(); second = new T(); // Error
}
类型擦除将 T 改变成 Object,你的本意肯定不希望调用 new Object(),所以编译器阻止了这个行为。
在 Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
Pair<String> p = new Pair.makePair(String::new);
makePair 方法接受一个函数式接口,例如 Supplier
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
还可以通过比较传统的反射来构造对象。
遗憾的是,细节有点复杂。不能调用:
first = T.class.newInstance(); // Error
表达式 T.class 是不合法的,因为它会擦除为 Object.class。
正确写法为:
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.newInstance(), clnewInstance());
} catch (Exception ex) {
return null;
}
}
就可以这样来调用:
Pair<String> p = Pair.makePair(String.class);
Class 类本身是泛型。例如,String.class 是一个 Class
的实例(事实上,它是唯一的实例)。
泛型类的静态上下文中类型变量无效
不能在泛型类中的静态域或方法中引用类型变量。例如:
public class Singleton<T> {
private static T singleInstance; // Error
public static T getSingleInstance() { // Error
}
}
注意,静态泛型方法与泛型类中的静态方法不是一回事:
public static T getSingleInstance() {...}
public static <T> T getSingleInstance() {...} // 泛型方法
不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展 Throwable 都是不合法的。例如,以下定义就不能正常编译:
public class Problem<T> extends Exception {...} // Error -- can't extend Throwable
catch子句中不能使用类型变量。例如,以下方法将不能编译:
public static <T extends Throwable> void doWork(Class<T> t) {
try {
do work
} catch (T e) { // Error -- can't catch type variable
}
}
不过,在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t) throws T { // OK
try {
do work
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
可以消除对受查异常的检查
Java 异常处理的一个基本原则是,必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。关键在于以下方法:
@SuppressWarnings("uncheched")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw (T) e;
}
假设这个方法包含在类 Block 中,如果调用:
Block.<RuntimeException>throwAs(t);
编译器就会认为 t 是一个非受查异常。
以下代码会把所有异常都转换为编译器所认为的非受查异常:
try {
do work
} catch (Throwable t) {
Block.<RuntimeException>throwAs(t);
}
下面把这个代码包装在一个抽象类中。用户可以覆盖body 方法来提供一个具体的动作。调用 toThread 时,会得到 Thread 类的一个对象,它的 run 方法不会介意受查异常。
public abstract class Block {
public abstract void body() throws Exception;
public Thread thThread() {
return new Thread() {
public void run() {
try {
body();
} catch (Throwable t) {
Block.<RuntimeException>throwAs(t);
}
}
};
}
@SuppressWarnings("uncheched")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw (T) e;
}
}
可以用上述类运行一个线程,让其抛出一个受查异常:
public static void main(String[] args) {
new Block() {
public void body() throws Exception {
Scanner in = new Scanner(new File("ququx", "UTF-8"));
while (in.hasNext()) System.out.println(in.next());
}
}.toThread().start();
}
运行上述代码,会得到一个栈轨迹,其中包含一个 FileNotFoundException(当假设你没有提供一个名为 ququx 的文件)。
这有什么意义呢?正常情况下,你必须捕获线程 run 方法中的所有受查异常,把它们「包装」到非受查异常中,因为 run 方法声明为不抛出任何受查异常。
不过在这里并没有做这种「包装」。我们只是抛出异常,并「哄骗」编译器,让它认为这不是一个受查异常。
通过使用泛型类、擦除和 SuppressWarnings 注解,就能消除 Java 类型系统的部分基本限制。
注意擦除后的冲突
有些时候,一个看似正确定义的方法会无法通过编译。例如:
public class Pair<T> {
public boolean equals(T t) {
return first.equals(value) && second.equals(value);
}
}
但是根据擦拭规则,equals(T)
就是 equals(Object)
。而这个方法是继承自 Object
的,与 Object.equals 发生冲突,编译器会阻止一个实际上会变成覆写的泛型方法定义。
补救方法就是重新命名引发错误的方法:
public class Pair<T> {
public boolean same(T t) {
return first.equals(value) && second.equals(value);
}
}
泛型规范说明还提到另外一个原则:「要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。」例如,下述代码是非法的:
class Employee implements Comparable<Employee> {...}
class Manager extends Employee implements Comparable<Manager> {...} // Error
Manager 会实现 Comparable
这一限制与类型擦除的关系并不十分明确。毕竟,下列非泛型版本是合法的:
class Employee implements Comparable {...}
class Manager extends Employee implements Comparable {...} // Error