泛型程序设计(二)
泛型代码和虚拟机
虚拟机没有泛型类型对象。这句话可以理解为虚拟机不认识泛型类,它仍然只认识普通类。
(一)类型擦除
什么是类型擦除?
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。而编译器在编译过程中去掉类型变量,就叫擦除。
什么是补偿?
编译器在将泛型类编译成class文件的过程中,擦除了类型变量,但是并用限定类型来替换原始类型的过程就叫补偿。如果没有限定的变量就用Object。
为什么要有擦除?
因为泛型在实现的早期版本中,是没有泛型的,Java虚拟机也不认识泛型,Java在jdk1.5后,才加入泛型,这时要让Java虚拟机再认识泛型,需要修改很多代码,也考虑到类加载器的兼容性,所以放弃了。也就有了擦除,所以也有人说Java泛型是伪泛型。
比如Pair类,因为T是没有限定的变量,所以用Object替换,我们来看看擦除后的代码:
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;}
}
如果是有限定的类型变量呢?原始类型用第一个限定的类型变量来替换,如果没有就用Object来替换。
例如:
public class Inteval<T extends Comparable&Serializable> implements Serializable
{
private T lower;
private T upper;
public Inteval(T first,T second)
{
if(firsr.compareTo(second)<=0){lower = first;upper=second;}
else{lower = second;upper=first;
}
}
//进行擦除和补偿后,原始类型如下:
public class Inteval implements Serializable
{
private Comparable lower;
private Comparable upper;
public Inteval(Comparable first,Comparable second)
{
if(firsr.compareTo(second)<=0){lower = first;upper=second;}
else{lower = second;upper=first;
}
}
(二)翻译泛型表达式
我们说过Java虚拟机不认识泛型代码。也就是说当我们调用泛型方法或域时,它都要对代码进行翻译。其实这个过程中,编译器做了两件事:
- 对原始方法Pair.getFirst的调用
- 将返回的Object类型强制转换String类型
我们用代码来看看编译器做的事情:
Pair<Employee> pe = ....;
Employee buddy = pe.getFirst();
//事实上,我们没有泛型时,需要进行强制类型转换
Employee buddy = (Employee)pe.getFirst();//编译器帮我们做了
(三)翻译泛型方法
类型擦除也会出现在泛型方法中,我们看下面一个简单的例子:
public static <T extends Comparable> T min(T[] a);
//进行擦除后
public static Comparable min(Comparable[] a);
但是方法擦除后,也带来了问题。看看这个例子:
class DateInteval extends Pair<LocalDate>{
public void setSecond(LocalDate second){
if(second.compareTo(getFirst())>=0){
super.setSecond(second);
}
}
...
}
当编译器擦除后,我们再看看DateInteval类和Pair类是什么样的?
//擦除后的Pair类
class Pair{
...
public void setSecond(Object second){
...
}
}
//擦除后的DateInteval类
class DateInteval extends Pair{
...
public void setSecond(LocalDate second){
...
}
}
我们看到,Pair类和DateInteval类都有一个setSecond方法,但是参数类型不一样,证明它是不同的方法。再看看调用时:
DateInteval inteval = new DateInteval(...);
Pair p = inteval;
p.setSecond(aDate);
我们看到p.setSecond(aDate);这段代码找到的是setSecond(Object)方法,但是p引用的是DateInteval的对象,所以应该调用的是setSecond(LocalDate)方法。这时,在擦除和多态时,就发生了冲突。要解决这个问题,就需要编译器在DateInteval类生成一个桥方法(bridge method)。
public void setSecond(Object second){
setSecond((Date) second);
}
变量pair已经声明为类型Pair<LocalDate>,并且这个类型只有一个简单方法叫setSecond,即setSecond(Object);虚拟机调用pair引用的对象是DateInteval类型,因而会调用DateInteval.setSecond(Object)方法,这个方法是合成的桥方法,它能调用DateInteval.setSecond(Date)方法,这正是我们想要的。
综上所述,我们需要记住有关Java泛型转换的事实:
①虚拟机中没有泛型,只有普通的类和方法
②所有的类型参数都用它们的限定类型进行替换,如果没有限定,那就是Object
③桥方法是被合成来保持多态的
④为保持类型安全性,必要时插入强制类型转换
约束与局限性
(一)不能用基本类型实例化类型参数
不能用类型参数代替基本类型。因此,没有Pair<double>,只有Pair<Double>。其原因是类型擦除。
(二)运行时类型查询只适用于原始类型
虚拟机中没有泛型,它只认识原始类型。所以使用instanceof关键字查询对象是否属于某个泛型类型会得到一个编译器错误。
if(a instanceof Pair<String>)// Error
if(a instanceof Pair<T>) //Error
if(a instanceof Pair)// Ok
同样的道理,getClass方法总是返回原始类型。
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(stringPair.getClass()==employeePair.getClass())//they are equal
(三)不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Pair<String>[] table = new Pair<String>[10];//Error
因为擦除后,对于数组table而言,它的类型是Pair[],可以把它转换成Object[],数组会记住他的元素类型,如果再往其中存储String类型,它会抛出ArrayStoreException。
对于泛型类型,擦除会使这种机制无效:
objarray[0] = new Pair<Employee>();
能够通过数组存储检查,但仍会导致一个错误,所以不允许创建参数化类型数组。
(四)Varargs警告
当我们创建一个参数可变的方法时:
public static <T> void addAll(Collection<T> coll,T...ts)
{
for(t:ts) coll.add(t);
}
我们知道ts实际上是一个数组,包含提供的所有实参。那么如果我们的ts添加的是泛型类型如Pair<String>,为了调用这个方法,虚拟机就会将所有的参数t建立一个数组,也就是含有类型参数的数组,这样就违反了上一个规则。当然,对于这样的调用,规则有所放松,它不是错误,而是警告。
可以采用两种办法来抑制这个警告,使用注解:
①使用@SuppressWarnings("unchecked")标注addAll方法
②在JavaSE7中,可以用@SafeVarargs标注addAll方法
(五)不能实例化类型变量
就是不能使用像new T(...),new T[...]或T.class这样的表达式中的类型变量。
(六)不能构造泛型数组
就像不能实例化类型变量一样,也不能构造这样的数组:
T[] mm = new T[10];
(七)泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量,例如:
public class Singleton<T>{
//禁止使用带有类型变量的静态域和方法
private static T singleton;//Error
public static T getSingleton(){ //Error
if(singleton==null){
construct new instance of T
}
return singleton;
}
}
(八)不能抛出或捕获的实例
既不能抛出也不能捕获泛型类对象。事实上,泛型类扩展Throwable都是不合法的。就是说我们不能在catch子句中使用类型变量。不过在异常规范中使用类型变量时可以的。
try
{
...
}
catch (T t) //这是错误的,不能捕获泛型类对象
{
...
}
//在异常规范中使用类型变量时允许的
public static <T extends Throwable> void doword(T t) throws T //Ok
(九)可以消除对受查异常的检查
通过使用泛型类、擦除和@SuppressWarning注解,就能消除Java类型系统的部分基本限制,如可以消除对受查异常的检查。
(十)注意擦除后的冲突
泛型规范说明还提到另一个原则:要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。
class Employee implements Comprable<Employee>{...}
class Manager extends Employee implements Comparable<Manager>{...}
Manager会实现Comparable<Employee>和Comparable<Manager>,这就是同一接口的不同参数化。
泛型类型的继承规则
如Employee类和Manager类有继承关系,但是Pair<Employee>和Pair<Manager>是没有任何关系的。下面的代码将不能编译成功:
Manager[] ms = ...;
Pair<Employee> pe = ArrayAlg.minman(ms);//Error 这是不能编译通过的
通配符类型
(一)通配符概念
通配符就是用"?"代替,通配符类型中,允许类型参数变化。例如:
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是Pair<String>。因为String类型不是Employee的子类。
(二)通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加能力,即可以指定一个超类型限定:
Pair<? super Manager>
表示类型变量可以是Manager或者是Manager的父类。
(三)无限定通配符
还可以使用无限定通配符。如:Pair<?>;注意这是与原始Pair类型不同的。类型Pair<?>有以下方法:
? getFirst();
void setFirst(?);
getFirst()的返回值只能是Object,setFirst方法不能被调用,甚至都不能用Object调用。Pair和Pair<?>的本质不同就是:可以用任意Object对象调用原始Pair类的setObject方法。
(四)通配符捕获
编写一个交换元素的方法:
public static void swap(Pair<?> p)
在调用这个方法的时候,通配符不是类型变量,所以"?"不能作为一种类型,也就是说下面的代码是错误的:
? t = p.getFirst();//Error
p.setFirst(p.getSecond());
p.setSecond(t);
这样就出现了问题,那么我们如何解决这个问题呢?我们提供了一个辅助方法:
public static <T> void swapHelper(Pair<T> p)
{
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
那么如果想要调用swap方法,可以通过swapHelper方法:
public static void swap(Pair<?> p)
{
swapHelper(p);
}
这个就类似于桥方法,这样我们就将“通配符”捕获了,在这种情况下,通配符捕获是不可避免的。