Java读书笔记-泛型(二)
泛型代码和虚拟机
虚拟机中并不存在泛型类型对象
在早期实现泛型 的时候,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文件。但是在后期被放弃了
类型擦除
定义泛型类型的时候,虚拟机都会提供一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型名。。擦除泛型变量,并替换为限定类型。,如果之前没有限定变量类型,那么会替换为Object,例如之前的Pair擦除后会变为:
Public class Pair
{
Private Object first;
Private Object first;
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替换
因为T没有被限定类型,所以直接用Object替换,在程序中可以包含不同类型的Pair,例如,javaPair<String>
或Pair。而擦除类型后就变成原始的Pair类型。
假设声明了不同的类型,如下:
public class Interval<T extends Comparable &Serializable> implements Serializable
{
private T lower;
public Interval(T first,T second){}
}
public class Interval implements Serializable
{
private Comparable lower;
private Comparable upper;
public Interval(Comparable first,Comparable second){}
}
翻译泛型表达式
现在我们知道在虚拟机中泛型代码会被擦除,那么擦除返回类型以后编译器会怎么做?
Pair<Employee> buddies=…;
Employee buddy=buddies.getIfrst();
擦除getfirst的返回类型后将返回Object。编译器自动插入强制转换
也就是变成:
1.对原始方法Pair.getFirst调用
2.讲返回的Object类型强制转换为Employee
保证多态的“桥方法”
方法的擦除带来了两个复杂问题:
class DateInterval extends Pair<LocalDate>
{
public void setSecond(LocalDate second)
{
if(( second.compareTo( getFirst() )>=0 )
super.setSecond(second);
}
}
一个日期区间是一对LocalDate对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除之后变成
class DateIntervalextends Pair
{
public void setSecond(LocalDate second)
{
}
}
令人感到奇怪的是,假设存在另一个从Pair继承的SetSecond方法,即
public void setSecond(Object second)
这是一个不同的方法吗?它有不同类型的参数-Objcet。然而,请看下列的语句序列
DateInterval interval =new DateInterval(…);
Pair<LocalDate> pair=interval;
Pair.setSecond(aDate);
这里希望对setSecond的调用具有多态性,即由于Pair引用的是DateInterval对象,所以应该调用DateInterval.setSecond0
但是这里类型擦除和多态发生了冲突。要解决这个问题,就需要编译器在DateInterval中生成一个桥方法(bridge method)
Public void setSecond(Object second)
{
setSecond((Date) second));
}
通过跟踪pair.setSecond(aDate)语句,我们可以了解它的工作过程:
1.变量Pair已经声明为类型Pair<LocalDate>
,并且这个类型只有一个简单的方法叫setSecond,即setSecond(Object)。
2.虚拟机用Pair引用的对象调用这个方法。这个对象是DateInterval类型的。所以会调用DateInterval.setSecond(Object)
方法。
3.这个方法就是编译器生成的合成的桥方法。它调用DateInterval.setSecond(Date),这是我们期望得到的操作。
假设getSecond被DateInterval覆盖:
class DateInterval extends Pair<LocalDate>
{
public LocalDate getSecond()
{
return (Date) super.getSecond().clone();
}
}
那么在这个类中,有两个getSecond方法。
LocalDate getSecond()//defined in DateInterval
Object getSecond()//overrides the method defined in Pair to call the first method
实际上是不能编写这样的代码(具有相同形参的两个方法不合法)。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型方法不同的字节码。虚拟机可以正确的处理这种情况。
我们要记住有关泛型转换的事实:
1. 虚拟机中没有泛型,只有普通的类和方法
2. 所有的类型参数都用它们的限定类型来替换
3. 桥方法都被合成来保持多态
4. 为了保证类型安全,必要的时候插入强制类型转换
约束与局限性
不能用基本类型实例化类型参数
不能用类型参数替代基本类型。因此,没有Pair<double>
,只有Pair<Double>
。因为其原因是类型擦除。擦除之后Pair<Object>
,而Object不能存储double值
RTTI(运行时类型查询)只适用于原始类型。
例如
if(a instanceof Pair<String<)//Error
If(a instanceof Pair<T>) //Error
Pair<String> p=(Pair<String>) a; //Warning--can only test that a is a Pair
为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,如果使用instanceof会出现编译错误,如果使用类型强转会发出警告。
同样的道理,getClass方法总是返回原始类型。例如:
Pair<String> stringPaIR=…;
Pair<Employee> employeePair=…; If(StringPair.getClass()==employeePair.getClass()) //两者相等
比较结果是true,因为两次getClass都返回Pair.class
不能创建参数化类型的数组。
不能实例化参数化类型的数组,例如:
Pair<String>[] table=new Pair<String>[10];//error
为什么呢?擦除之后 table的类型是Pair[],可以转换为Object[];
Object[] objarray=table;
但是数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出ArrayStoreException异常。
如果存储一个泛型类型元素,擦除会使这种机制无效。以下赋值:
Objarray[0]=new Pair<Employee>();
虽然能通过数组检查,但是还是会导致类型错误。所以不允许创建参数化类型的数组。
如果需要手机参数化类型对象,只有一种安全而有效的方法:使用ArrayList: ArrayList<Pair<String>>
.
不能实例化类型变量
不能使用类似于 new T(…),new T[..]或T.class这样的表达式中的类型变量。例如
Public Pair()
{
first=new T();
Second=new T();
}//Error
不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。
例如:
public static <T extends Comparable> T[] minimax(T[] a)
{
T[] mm =new T[2];
…
}//error
类型擦除会让这个方法永远构造Comparable[2]数组。
有一种比较老式的方法是使用反射来达到目的,调用Array.newInstance
public static <T extends Comparable>T[] minmax(T..a)
{
T[] mm=(T[])Array.newInstance(a.getClass().getComponentType(),2);
}
泛型类的静态上下文中类型变量无效
不能在静态域或者方法中引用类型变量。例如,下列方法将无法施展:
public class Singleton<T>
{
private static T singleInstance; //error
public static T getSingleInstance()//error
}
为什么呢?因为擦除以后,只剩下Singleton ,如果可以运行这个程序,就可以声明一个Singleton<Random>
和Singleton<Date>
。但是它无法工作
不能抛出或捕获泛型的实例
即不能抛出也不能捕获泛型类对象,实际上,泛型类扩展Throwable都是不合法的。catch子句中也不能使用类型变量,不过,在异常规范中使用类型变量是允许的:
public class Problem<T> extends Exception //Error
public static <T extends Throwable> void doWork(Class<T> t)
{
try{
Do work
}
catch(T e) //Error-- can't catch type varible
{}
}
public static <T extends Throwable> void doWork(T t) throws T //OK
{
….
catch(Throwable realCause)
{
t.initCause(realCause);
yhrow t;
}
注意擦除后的冲突
假设像下面这样将equals方法添加到Pair中:
public class Pair<T>
{
public boolean equals(T value) { return first.equals(value) && second.equals(value);
}
考虑一个Pair<String>
从概念上来说,它有两个equals方法boolean equals(String)``````boolean equals(Object)
但是,方法擦除 boolean equals(T)
就是boolean equals(Object)
,补救的方法就是重新命名引发错误的方法。
泛型规范说明还提到另外一个原则:“要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。”例如,,下列方法非法:
class Employee implements Comparable<Employee>{…}
class Manager extends Employee implements Comparable<Manager> //Error
Manager会实现Comparable<Employee>
和 Comparable<Manager>
,这是同一接口的不同参数化。
这一限制与类型擦除的关系并不十分明确。毕竟,下列非泛型版本是合法的。
class Employee implements Comparable{..}
class Manager extends Employee implements Comparable{..}
其原因非常微妙,有可能与合成的桥方法产生冲突。实现了Comparable的类可以获得一个桥方法:
public int compareTo(Object other)
{
return compareTo((X) other);
}
对于不同类型的X不能有两个这样的方法。