在泛型代码内部,无法获得任何有关泛型参数类型的信息。因此,我们可以知道类型参数标识符(如T)和泛型类型边界的信息,却不能知道某个特定实例的实际的类型参数。Java的泛型是使用擦除来实现的,即List<String>和List<Integer>都被擦除为List。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
public class ErasedType {
public static void main(String[] args) {
//比较ArrayList<String>和ArrayList<Integer>的类型
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
//输出map的参数信息
HashMap<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
}
}
/*输出为
true
[K, V]*/
在擦除的实现中,泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型都被擦除,替换为它们的非泛型上界。如List<T>被擦除为List,未指定上界的普通类型变量被擦除为Object。
可以看到,擦除减少了泛型的泛化性。然而Java仍然使用擦除的主要原因就是泛型不是一开始就支持的特性,而JavaSE 5时才支持的泛型必须可以向上兼容,要使得泛化的客户端可以用非泛化的类库来使用,或者反之亦然。
C++模板与Java泛型的对比
Java泛型的擦除与C++对模板的实现是大不相同的,在C++中我们看如下代码:
#include <iostream>
using namespace std;
template <class T> class Manipulator {
T obj;
public:
Manipulator(T x) {
obj = x;
}
void manipulate() {
obj.f();
}
};
class HasF {
public:
void f() {
cout << "aaa" << endl;
}
};
int main() {
HasF hf;
Manipulator<HasF> m(hf);
m.manipulate(); //可以正常输出aaa
return 0;
}
manipulate()方法在obj上调用f(),它怎么知道T类型的obj有f()方法呢?原因是当实例化模板时,C++编译器会进行检查。因此在Manipulator被实例化时,它看到HasF有一个方法f()。如果没有则会得到一个编译报错,因此总是安全的。
再把上面的代码写成Java则编译不会通过。
class HasF {
public void f() {
System.out.println("aaa");
}
}
class Manipulator<T> {
private T obj;
public Manipulator(T x) {
obj = x;
}
public void manipulate() {
obj.f(); //这句报错
}
}
public class Test {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> m = new Manipulator<HasF>(hf);
m.manipulate();
}
}
由于有了擦除,Java编译器无法将T类型的obj要调用f() 对应到 HasF有用f() 这一事实。为了调用f(),我们必须协助泛型类,给定泛型类的边界。即将Manipulator的声明写为class Manipulator<T extends HasF>。边界声明:T必须是HasF或其子类,此时obj就一定有f()可用了。
关于泛型数组
不能用一般的语法创建泛型数组
T[] array = new T[SIZE]; //错误
T[] array = (T)new Object[SIZE]; //可以通过,但是会出现unchecked warning
用这个注解@SuppressWarnings("unchecked")
关闭对泛型数组转型的警告
由于擦除,数组的运行时类型就只能是Object[]。如果我们立即将其转型为T[],那么在编译器该数组的实际类型就会丢失,而编译器就可能会错过某些错误检查。因此最好是使用Object[],然后当要使用数组时,添加一个向T[]的转型,即:
class GenericArray<T> {
private Object[] array;
//其他代码···
//需要使用时
array = (T[])array;
}
泛型不能用于显式地引用运行时类型的操作 之中,例如转型、instanceof、new表达式,因为所有关于参数的类型信息都丢失了。
总之,无论如何,在编写泛型代码时,必须时刻提醒自己,只是看起来拥有有关参数的类型信息而已。比如Manipulator<HasF> m = new Manipulator<HasF>(hf);
这句,尽管语法好像表示所有的T都被替换为HasF,但我们必须时刻提醒自己:它只是一个Object罢了。