铁文整理
12.6 约束与局限性
在下面几节中,将阐述使用Java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。
12.6.1 不能用基本类型实例化类型参数
不能用类型参数代替基本类型。因此,没有Pair<double>,只有Pair<Double>。当然,其原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储doubIe值。
这的确令人烦恼。但是,这样做与Java语言中基本类型的独立状态相一致。这并不是一个致命的缺陷——只有8种基本类型,当包装器类型不能接受替换时,可以使用独立的类和方法处理它们。
12.6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型査询只产生原始类型。例如,
if (a instanceof Pair<String>) // same as a instanceof Pair
实际上仅汉测试a是否是任意类型的一个Pair。下面的测试同样为真
if (a instanceof Pair<T>) // T is ignored
或强制类型转换
Pair<String> p = (Pair<String>) a; // WAftNINC-can only test that a is a Pair
要记住这一风险,无论何时使用instanceof或涉及泛型类型的强制类型转换表达式都会看到一个编译器警告。
同样的道理,getClass方法总是返回原始类型。例如,
Pair<String> strigPair = ...;
Pair<Employee> employeePair = ...;
if (strigPair.getClass() == employeePair.getClass()) // they are equal
其比较的结果是true,这是因为两次调用getClass都将返回Pair.class。
12.6.3 不能抛出也不能捕获泛型类实例
不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法。例如,下面的定义将不会通过编译:
public class Problem<T> extends Exception { ... } // ERROR--can't extend Throwable
不能在catch子句中使用类型变量,例如,下面的方法将不能通过编译:
public static <T extends Throwable> void doWork(C1ass<T> t) {
try {
doSomeWork();
} catch (T e) // ERROR-—can't catch type variable
{
Logger.global.info(...);
}
}
但是,在异常声明中可以使用类型变量。下面这个方法是合法的:
public static <T extends Throwable> void doWork(T t) throws T // OK
{
try {
doSomeWork();
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
12.6.4 参数化类型的数组不合法
不能声明参数化类型的数组,如:
Pair<String>[] table = new Pair<String>[10]; // ERROR
这样做有什么问题呢?擦除之后,table的类型是Pair[],可以将其转换为Object[]:
Object[] objarray = table;
数组能够记住它的元素类型,如果试图存入一个错误类型的元素,就会抛出一个ArrayStoreException异常:
objarray[0] = "Hello"; // ERROR-—component type is Pair
但是,对于泛型而言,擦除将会降低这一机制的效率。赋值
objarray[0] = new Pair<Employee>();
可以通过数组存储的检测,但仍然会导致类型错误。因此,禁止使用参数化类型的数组。
提示:如果需要收集参数化类型的对象,最好直接使用ArrayList: ArrayList<Pari<String>>,这样既安全又有效。
12.6.5 不能实例化类型变量
不能使用像new T(...)、new T[...]或T.class这样的表达式中的类型变量。例如,下面的Pair<T>构造器就是非法的:
public Pair() {
first = new T();
second = new T();
} // ERROR
类型擦除将T改变成Object,而且,本意肯定不希望调用new Object()。但是,可以通过反射调用Class.newInstance方法来构造泛型对象。遗憾的是,细节有点复杂。不能调用:
first= T.class.newInstance(); // ERROR
表达式T.class是不合法的。必须像下面这样设计API以便可以支配Class对象:
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<T>(cl.newInstance(), cl.newInstance());
} catch (Exnption ex) {
return null;
}
}
这个方法可以按照下列方式调用:
Pair<String> p = Pair.makePair(String.class);
注意,Class类本身是泛型。例如,String.class是一个Class<String>的实例(事实上,它是惟一的实例)。因此,makePair方法能够推断出pair的类型。
不能构造一个泛型数组:
public static <T extends Comparable> T[] minmax(T[] a) {
T[] mm = new T[2]; // ERROR
}
类型擦除会让这个方法永远构造Object[2]数组。
如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并且在获取元素时进行类型转换。例如,ArrayList类可以这样实现:
public class ArrayList<E> {
private Object[] elements;
@SuppressWarnings("unchecked")
public E get(int n) {
return (E) elements[n];
}
public void set(int n, E e) {
elements[n] = e;
}// no cast need
}
实际的实现没有这么清晰:
public class ArrayList<E> {
private E[] elements;
public ArrayList() {
elements = (E[]) new Object[10];
}
}
这里,强制类型转换E[]是一个假象,而类型擦除使其无法察觉。
由于minmax方法返回T[]数组,使得这一技术无法施展,如果掩盖这个类型会有运行时错误结果,假设执行代码:
public static <T extends Comparable> T[] minmax(T[] a) {
Object[] mm = new Object[2];
return (T[]) mm;// compiles with warning
}
调用String[] ss = minmax("Tom", "Dick", "Mary");编译时不会有任何警告。当Object[]引用赋给String[]变量时,将会发生ClassCastException异常。
在这种情况下,可以利用反射,调用Array.newInstance:
public static <T extends Comparable> T[] minmax(T[] a) {
T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
}
ArrayList类的toArray方法就没有这么幸运。它需要生成一个T[]数组,但没有成分类型。因此,有下面两种不同的形式:
Object[] toArray()
T[] toArray(T[] result)
第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用result的成分类型构造一个足够大的新数组。
12.6.6 泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。例如,下列高招将无法施展:
public class Singleton<T> {
public static T getSingleInstance() // ERROR
{
if (singleInstance == null) constructNewInstanceOfT():
return singleInstance;
}
private static T singleInstance; // ERROR
}
如果这个程序能够运行,就可以声明一个Singleton<Random>共享随机数生成器,声明一个Singleton<JFileChooser>共享文件选择器对话框。但是,这个程序无法工作。类型擦除之后,只剩下Singleton的类,它只包含一个singleInstance域,因此,禁止使用带有类型变量的静态域和方法。
12.6.7 注意擦除后的冲突
当泛型类型被擦除时,无法创建引发冲突的条件。下面是一个示例。假定像下面这样将eqals方法添加到Pair类中:
public class Pair<T> {
public boolean equals(T value) {
return first.equals(value)&& second.equals(value);
}
}
考虑一个Pari<String>。从概念上讲,它有两个equals方法:
boolean equals(String) // deftned in Pair<T>
boolean equals(Object) // Inherited from Object
但是,直觉把我们引入歧途。方法擦除后,boolean equals(T)就是boolean equals(Object)。与Object.equals方法发生冲突。
当然,补救的办法是重新命名引发错误的方法。
泛型规范说明还提到另外一个原则:“要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。”例如,下述代码是非法的:
class Calendar implements Comparable<Calendar> { ... }
class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar> { ... } // ERROR
GregorianCalendar会实现Comparable<Calendar>和Comparable<GregorianCalendar>,这是同一接口的不同参数化。
这一限制与类型擦除的关系并不十分明确。毕竟,下列非泛型版本是合法的。
class Calendar implements Comparable { ... }
class GregorianCalendar extends Calendar implements Comparable { ... } // ERROR
其原因非常微妙,有可能与合成的桥方法产生冲突。实现了Comparable<X>的类可以获得一个桥方法:
public int compareTo(Object other) {
return compareTo((X) other);
}
对于不同类型的X不能有两个这样的方法。