泛型的约束与局限性
由于泛型是通过类型擦除、强制类型转换和桥方法来实现的,所以存在某些局限(大多来自于擦除)。
不能使用基本类型实例化类型参数
类型参数都是类,要用包装器将基本类型包装才可以作为类型参数(原因在于擦除类型后Object
类不能存储基本类型的值)。当包装器类不能接受类型参数替换时,可以使用独立的类和方法进行处理。
运行时类型查询只适用于原始类型
由于虚拟机中的对象都有特定的原始类型,所以类型查询只能查询原始类型。
// 只能测试a是否为某种Pair类型
if (a instanceof Pair<String>) {...}
if (a instanceof Pair<T>) {...}
Pair<String> p = (Pair<String>) a;
当代码试图使用instanceof
查询对象是否属于泛型类型时,编译器会报错;如果使用强制类型转换会得到警告。
同样,即使使用getClass
方法也只能返回原始类型。
不能创建参数化类型的数组
由于擦除类型后,只剩下原始类型,所以如果允许创建参数化类型的数组的话,就可以在之后将这个数组向上转型为Object[]
然后向其中添加提供的参数类型以外的参数类型定义的泛型对象。这违背了类型安全的要求。
Pair<String> table = new Pair<String>[10];
Object[] = objArray = table;
// objArray记住了table的原始类型Pair,因此存储String类会抛出ArrayStoreException异常
objArray[0] = "Hello";
// objArray只记住了Pair,因此添加Pair<Employee>也不会编译报错,但会导致类型错误
objArray[0] = new Pair<Employee>();
注:不允许创建数组不代表不允许声明数组变量,只是不能用new
来对其初始化。
注2:可以声明通配类型的数组,然后通过类型转换来初始化变量,但这仍是不安全的。
提示:唯一安全有效地收集参数化类型对象的方法是ArrayList:ArrayList<Pair<String>>
。
Varargs警告
当向一个参数个数可变的方法传递泛型类型实例时,实际上虚拟机必须建立一个参数化类型的数组作为方法的参数,这就违反了不允许创建参数化类型数组的规定。不过编译器只会发出警告,而不是错误。
能够确定调用正确时,可以使用注解或@SafeVarargs
标注方法来取消警告。
不能实例化类型变量
不能实例化T
类型,因为其会被擦除为Object
类型。
最好的解决办法是让调用者提供一个构造器表达式,即设计一个方法接收Supplier<T>
函数式接口来获取T
类型实例。此时T
是由传入的函数式接口决定的,不必担心被擦除。
// 向该方法传入构造器引用即可获取T类型的实例
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
不能构造泛型数组
存在类型限制的情况下,类型擦除会导致生成的数组总是限制类型。
如果泛型数组仅作为类的私有实例域,可以将这个数组声明为Object[]
,在获取元素时进行类型转换即可。
如果泛型数组别有它用,应当提供构造器引用来生成正确类型的数组。方法与实例化泛型变量类似。
泛型类的静态上下文中类型变量无效
如果设计一个返回静态泛型域的静态方法,类型擦除会导致该方法返回Object
类型的静态域。由于该静态域会被该泛型类中的所有对象共享,所以可能无法应用到对象上。
// 擦除类型后静态变量为Object类型
public class Singleton<T> {
private static T singleInstance;
private static T getSingleInstance() {
if (singleInstance == null) // construct new instance of T
return singleInstance;
}
}
// 无法确定返回类型
SingletonA = new Singleton<Integer>();
SingletonB = new Singleton<Boolean>();
Singleton.getSingleInstance();
不能抛出或捕获泛型类的实例
泛型类禁止扩展Thorwable
接口。但泛型类型T
可以扩展Throwable
接口,但不能捕获或抛出。
// 不合法
public static <T extends Throwable> void doWork(Class<T> t) {
try {
// doWork
}catch (T e) {
Logger.global.info(...);
}
}
// 合法
public static <T extends Throwable> void doWork(T t) throws T {
try {
// doWork
}catch (Throwable realCause) {
t.initCause(realCause);
// t被擦除为Throwable类,然后返回后被调用者强制转换为T类型
throw t;
}
}
可以消除对受查异常的检查
使用泛型可以同时为所有的受查异常提供处理器。
考虑一个线程程序:
package tClass;
public abstract class Block {
public abstract void body() throws Exception;
public Thread toThread(){
return new Thread() {
// run方法声明不抛出异常
public void run() {
// 将所有异常转换为编译器认为的非受查异常
try{
body();
}catch (Throwable t) {
Block.<RuntimeException>throwAs(t);
}
}
};
}
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw (T)e;
}
}
package tClass;
import java.io.File;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
new Block() {
// body方法打开不存在的文件,抛出受查异常FileNotFoundException
public void body() throws Exception {
Scanner in = new Scanner(new File("notExistedFile"), "UTF-8");
while(in.hasNext())
System.out.println(in.next());
}
}.toThread().start();
}
}
通过上述方法,不必捕获run
方法中的受查异常,只需令编译器认为抛出的是非受查异常。在run
方法产生多种异常的情况下,这种技术可以避免编写捕获多个异常并包装为非受查异常抛出的繁琐代码。
注意擦除后的冲突
当泛型类重载了超类中的方法时,若重载的参数类型是T
,则会被擦除为Object
。这可能会导致冲突。
public class Pair<T> {
// 被擦除为eqauls(Object value)与从Object类继承的equals(Object)冲突
public boolean equals(T value) {return first.equals(value) && second.eqauls(value);}
}
泛型规范原则:要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口时同一接口的不同参数化。
存在上述原则的原因在于:实现接口参数化的类会获得接口的桥方法,如果实现两个同一接口的不同参数化,就会获得两个签名完全相同的桥方法,导致冲突。
原文地址:https://www.cnblogs.com/aries99c/p/12616634.html