前言
本文为对Java泛型技术类型擦除部分的一个总结,主要参考文献有《Java编程思想 第4版》、《Java核心技术 第10版》、《深入理解Java虚拟机 第2版》,文中的代码Demo主要来自于Java编程思想。
C++模板
下面是使用模板的C++示例。
#include
class HasF {
public:
void f() { std::cout << "HasF::f()" << std::endl; }
};
template class Manipulator {
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
int main(int argc, const char * argv[]) {
HasF hf;
Manipulator manipulator(hf);
manipulator.manipulate();
return 0;
}
程序运行无误,输出
HasF::f()
当我们调用一个模板时,C++编译器用实参来为我们推断模板实参,并为我们实例化(instantiate)一个特定版本的代码。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。被编译器生成的版本通常称为模板的实例。
对于上面代码,在编译时,编译器会将T替换成HasF并生成模板实例,类似于这样。
class Manipulator {
HasF obj;
public:
Manipulator(HasF x) { obj = x; }
void manipulate() { obj.f(); }
};
Java泛型有点不太一样
用C++编写这种代码很简单,因为当模板实例化时,模板代码知道其模板参数的类型,Java泛型就不同了。下面是HasF的Java版本。
// HasF.java
public class HasF {
public void f() { System.out.println("HasF.f()"); }
}
// Manipulation.java
class Manipulator {
private T obj;
public Manipulator(T x) { obj = x; }
public void manipulate() { obj.f(); } // 编译错误
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator manipulator = new Manipulator(hf);
manipulator.manipulate();
}
}
上面代码不能编译,报错
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method f() is undefined for the type T
从上面报错信息可以看出,编译器认为类型T没有f()方法。这是由于Java的泛型是使用擦除来实现的,意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
从表面上看,Java的泛型类类似于C++的模板类,唯一明显的不同是Java没有专用的template关键字,但是,其实这两种机制着本质的区别。
擦除法实现的伪泛型
Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。使用擦除法的好处就是实现简单、非常容易Backport,运行期也能够节省一些类型所占的内存空间。而擦除法的坏处就是,通过这种机制实现的泛型远不如真泛型灵活和强大。Java选取这种方法是一种折中,因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。
泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。
为了验证擦除,我们编写下面代码。
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1 == c2);
}
}
果然,执行结果为
true
尽管ArrayList
如果想要运行上面Java版本的HasF,必须协助泛型类,给定泛型的边界,以告知编译器只能接收遵循这个边界的类型。这里重用了extends关键字(与类的继承有点类似,但又不完全相同),给出类型的上界。之所以称为上界,是通过继承树来考虑的,对于继承树父节点在上,子节点在下,那么extends关键字就限定了类型最多能上了继承树的什么地方,也就是上界。由于有了边界,下面的代码就可以编译了。
public class Manipulator2 {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
}
泛型类型参数将擦除到它的第一个边界(可以有多个边界)。编译时,Java编译器会将T擦除成HasF,就好像在类的声明中用HasF替换了T一样。
那泛型有什么用
可能就有小伙伴疑惑了,上面的泛型好像并没有什么用,我直接写下面这种手动擦除的代码不行吗。
public class Manipulator3 {
private HasF obj;
public Manipulator3(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
}
这提出了很重要的一点:只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型)更加泛化化时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。
泛型参数和他们在有用的泛型代码中的应用,通常比简单的替换来的更复杂。例如,如果某个类有一个返回T的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型。
例如对于下面两种写法,泛型的写法可以不用强制转换类型。编译器会在编译期执行类型检查并插入转型代码。理解编译器对泛型的处理非常重要。
// SimpleHolder.java
public class SimpleHolder {
private Object obj;
public void set(Object obj) { this.obj = obj; }
public Object get() { return obj; }
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
Holder.set("Item");
String string = (String)Holder.get();
}
}
// GenericHolder.java
public class GenericHolder {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder holder = new GenericHolder<>();
holder.set("Item");
String s = holder.get();
}
}
其实它们生成的字节码是完全相同的。对进入set()的类型检查是不需要的,因为这是由编译器执行的,而对从get()返回的值进行转型仍旧是需要的。
public GenericHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public T get();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class GenericHolder
3: dup
4: invokespecial #4 // Method "":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method get:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
可以看出,使用泛型机制编写的代码要比哪些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型是我们需要的程序设计手段。
解决擦除的问题
擦除丢失了在泛型代码中执行某些操作的能力。任何运行时都需要知道确切的类型信息的操作都无法工作。
public class Erased {
private final int SIZE = 100;
public static void f(Object arg) {
if (arg instanceof T) {} // 编译错误
T var = new T(); // 编译错误
T[] array = new T[SIZE]; // 编译错误
}
}
下面给出一些方法解决上面的问题。
- 引入类型标签,使用动态的isInstance()代替instanceof
class Building {}
class House extends Building {}
public class ClassTypeCapture {
Class kind;
public ClassTypeCapture(Class kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture ctt1 = new ClassTypeCapture<>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture ctt2 = new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
运行结果
true
true
false
true
- 用工厂方法或模版方法创建类型实例
下面的两段代码是学习设计模式的好材料。先来看工厂方法模式。
interface FactoryI { T create(); }
class Foo2 {
private T x;
public > Foo2(F factory) {
x = factory.create();
}
}
class IntegerFactory implements FactoryI {
public Integer create() {
return new Integer(0);
}
}
class Widget {
public static class Factory implements FactoryI {
public Widget create() {
return new Widget();
}
}
}
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2(new IntegerFactory());
new Foo2(new Widget.Factory());
}
}
模板方法模式。
abstract class GenericWithCreate {
final T element;
public GenericWithCreate() { element = create(); }
abstract T create();
}
class X {}
class Creater extends GenericWithCreate {
X create() { return new X(); }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
public class CreatorGeneric {
public static void main(String[] args) {
Creater c = new Creater();
c.f();
}
}
- 用ArrayList代替数组,或者是传入类型标记
public class GenericArrayWithTypeToken {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class type, int sz) {
array = (T[])Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArrayWithTypeToken gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
Integer[] ia = gai.rep();
}
}
真的完全擦除了吗
在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。