Java中的泛型代码和C++中的模板有一个很大的不同:C++中模板的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。
Java中并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类。
虚拟机中的泛型转换需要记住4条事实:
1) 定义任何一个泛型都会自动产生其原始类型(raw type)
2) 这个过程中,泛型类型会被擦除,替换为其限定类型(bounding type)
3) 必要时插入强制转换来保证类型安全
4) 使用桥接方法(bridge method)来保证正确处理多态
例如写一个返回数组中最小值的泛型函数,可以如下定义。
public static <T extends Comparable> T min(T[] a)
(实际上Comparable也是一个泛型接口,所以最正确的定义方法是public static <T extends Comparable<? super T>> T min(T[] a) )
类型擦除后,替换为限定类型Comparable,其原始类型就是
public static Comparable min(Comparable[] a)
如果没有限定类型,那么类型擦除后就替换为Object。例如一个Pair<T>类
public class Pair<T> { private T first; private T second; public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } public void setFirst(T newValue) { first = newValue; } public void setSecond(T newValue) { second = newValue; } }
Pair<T>类执行类型擦除后的原始类型就是
public class Pair { private Object first; private Object second; public Pair(Object first, Object second) { this.first = first; this.second = second; } public Object getFirst() { return first; } public Object getSecond() { return second; } public void setFirst(Object newValue) { first = newValue; } public void setSecond(Object newValue) { second = newValue; } }
下面一段代码,
Pair<String> pair = new Pair<String>("1", "2");
String s = pair.getFirst();
类型擦除后,getFirst()返回Object。所以,编译器实际上把它转换为下面的虚拟机指令:
a) 调用原始类型的Pair.getFirst
b) 把返回的Object对象强制转换为String
类型擦除有时候会带来很复杂的情况:
例如下面写了一个新的类:
class DateInterval extends Pair<Date> { public DateInterval(Date first, Date second) { super(first, second); } public void setSecond(Date second) { if (second.compareTo(getFirst()) >= 0) { super.setSecond(second); } else { throw new IllegalArgumentException("Second date should be no earlier than first date."); } } public Date getSecond() { return (Date) super.getSecond().clone(); } }
类型擦除后,DateInterval中就有两个setSecond方法。
public void setSecond(Object second)
public void setSecond(Date second)
在这里,类型擦除和多态就产生了冲突。因为实际上我们是想要覆盖基类的方法。
为了解决这类问题,编译器会产生一个桥接方法
public void setSecond(Object second) {
setSecond((Date) second);
}
类型擦除后,DateInterval中有两个getSecond方法
public Date getSecond()
public Object getSecond()
在Java代码中,直接这么申明两个方法,是会导致编译错误的,因为不允许两个同名方法具有相同的参数类型。
但是,在虚拟机中,也就是编译后的代码中,参数类型加上返回类型来识别一个方法,所以这样做是允许的,因为返回类型不一样。
实际上,编译器生成这样一个桥接方法。
public Object getSecond() {
return getSecond();
}
可以使用Jad工具来反编译class文件,就能清除到看到编译器所做的工作。
例如运行jad.exe DateInterval.class,生成下面的反编译文件
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) // Source File Name: Pair.java package learning.generic; import java.util.Date; // Referenced classes of package learning.generic: // Pair class DateInterval extends Pair { public DateInterval(Date first, Date second) { super(first, second); } public void setSecond(Date second) { if(second.compareTo((Date)getFirst()) >= 0) super.setSecond(second); else throw new IllegalArgumentException("Second date should be no earlier than first date."); } public Date getSecond() { return (Date)((Date)super.getSecond()).clone(); } public volatile void setSecond(Object obj) { setSecond((Date)obj); } public volatile Object getSecond() { return getSecond(); } }