JAVA基础(11)——泛型

时间:2023-02-16 12:16:02

JAVA基础系列规划:


先看这样一段小程序:

public class GenericType {
public static void main(String[] args) {
ArrayList al = new ArrayList();
al.add("hello world!");
String s = al.get(0); //编译不通过
System.out.println(s);
}
}

当我们满怀喜悦地运行时,却发现程序并不能通过编译。一拍脑门,原来ArrayList的get方法定义为返回Object,要把Object赋给String,必须进行强制类型转换,好吧:

String s = (String)al.get(0); //编译通过

bingo!运行通过,”hello world!”

如果ArrayList中存储的不是String类型的元素,强制类型转换的时候就会出现ClassCastException的错误。

一方面要求强制类型转换,另一方面强制类型转换可能出错,是不是很头疼,你必须在使用数据时清楚地知道其数据类型,才能完成一次完美的强制类型转换。

你可能会想,很简单啊,问题就出在不知道容器里面数据的类型,要是能把容器中数据类型固定下来,这样就可以不使用强制转型了,也就不会有错误出现了。

你想的没错,这就是泛型要做的事情。

泛型原理 | JDK泛型容器类 | 泛型构造

1 泛型原理

泛型告诉编译器想使用什么类型,实现编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException,举个栗子:

public class GenericType {
public static void main(String[] args) {
ArrayList<String> al = new ArrayList<String>();
al.add("content");
String s = al.get(0); //编译通过
System.out.println(s);
}
}

这段代码指定了容器ArrayList中数据的类型为String(称这种方式为泛型),但实际上,编译器在编译时去掉了泛型中的类型信息(称这个过程为类型擦除),所以在运行时不存在任何类型相关的信息。

类型擦除的基本过程也比较简单:将类型参数替换为原始类型,同时去掉出现的类型声明,产生可能需要的桥接方法。类型擦除时,类型参数被限定类型(无限定的变量用Object)替换,同时去掉出现的类型声明,即去掉<>的内容。

class Pair<T> {  
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}

变为:

class Pair {  
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}

如果指定了类型参数的上界的话,则使用这个上界。

若public class Pair< T extends Comparable&Serializable>,则原始类型就是Comparable。

若public class Pair< T extends Serializable&Comparable> ,则原始类型就用Serializable替换,而编译器在必要的时要向Comparable插入强制类型转换。为了提高效率,应该将标签接口(即没有方法的接口)放在边界限定列表的末尾。

既然说类型变量会在编译的时候擦除掉,那为什么我们往

ArrayList<String> arrayList = new ArrayList<String>();

所创建的数组列表arrayList中,不能使用add方法添加整型呢?

不是说泛型变量Integer会在编译时候擦除变为原始类型Object吗,为什么不能存别的类型呢?

结论是:Java编译器是通过先对泛型引用进行类型检查,然后再进行类型擦除,再进行编译的。

再举个栗子:

public static  void main(String[] args) {  
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("123");
arrayList.add(123);//编译错误
}

在上面的程序中,使用add方法添加一个整型,在eclipse中,直接就会报错,说明这就是在编译之前的检查。因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

那么,这个类型检查是针对谁的呢?我们先看看参数化类型与原始类型的兼容,用ArrayList再举个栗子:

ArrayList arrayList = new ArrayList(); //以前的写法
ArrayList<String> arrayList = new ArrayList<String>(); //现在的写法

如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况:

ArrayList<String> arrayList1 = new ArrayList(); //第一种情况,与完全使用泛型参数的效果一样
ArrayList arrayList2 = new ArrayList<String>(); //第二种情况,没有泛型效果

这样是没有错误的,不过会有个编译时警告。不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则完全没效果。

因为,本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用arrayList1 来调用它的方法,比如说调用add()方法,所以arrayList1引用能完成泛型类型的检查。而引用arrayList2没有使用泛型,所以不行。

举个栗子:

public class Test {  
public static void main(String[] args) {
ArrayList<String> arrayList1 = new ArrayList();
arrayList1.add("1");//编译通过
arrayList1.add(1);//编译错误
String str1 = arrayList1.get(0);//返回类型就是String

ArrayList arrayList2 = new ArrayList<String>();
arrayList2.add("1");//编译通过
arrayList2.add(1);//编译通过
Object object = arrayList2.get();//返回类型就是Object

new ArrayList<String>().add("11");//编译通过
new ArrayList<String>().add(22);//编译错误
String string = new ArrayList<String>().get(0);//返回类型就是String
}
}

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

你可以把List< String>传递给一个接受List< Object>参数的方法吗?泛型中的引用传递的问题?
结论是:泛型不是协变的。
对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List< String>应当可以用在需要List< Object>的地方,但是事实并非如此。真这样做的话会导致编译错误。

ArrayList<String> arrayList1 = new ArrayList<Object>();//编译错误  
ArrayList<Object> arrayList1 = new ArrayList<String>();//编译错误

如果你再深一步考虑,你会发现Java这样做是有意义的。

我们先看第一种情况,将第一种情况拓展成下面的形式:

ArrayList<Object> arrayList1 = new ArrayList<Object>();  
arrayList1.add(new Object());
arrayList1.add(new Object());
ArrayList<String> arrayList2 = arrayList1;//编译错误

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的。),可是它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。
再看第二种情况,将第二种情况拓展成下面的形式:

ArrayList<String> arrayList1 = new ArrayList<String>();  
arrayList1.add(new String());
arrayList1.add(new String());
ArrayList<Object> arrayList2 = arrayList1;//编译错误

没错,这样的情况比第一种情况好的多,最起码,在我们用arrayList2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,如果又用arrayList2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?

2 JDK泛型容器类

自动类型转换

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?

结论:在return之前,会根据泛型变量进行强转。

看下ArrayList和get方法:

public E get(int index) {  
RangeCheck(index);
return (E) elementData[index];
}

看以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date)elementData[index]。所以我们不用自己进行强转。

当存取一个泛型域时也会自动插入强制类型转换。假设Pair类的value域是public的,那么,表达式:

Date date = pair.value 

也会自动地在结果字节码中插入强制类型转换。

3 泛型构造

3.1 类型擦除与多态的冲突和解决方法

结论:由于擦除了类型之后的类可能缺少某些必须的方法,可能需要产生一些桥接方法。

现在有这样一个泛型类:

class Pair<T> {  
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}

然后我们想要一个子类继承它:

class DateInter extends Pair<Date> {  
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}

在这个子类中,我们设定父类的泛型类型为Pair< Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:

将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型:

public Date getValue() {  
return value;
}
public void setValue(Date value) {
this.value = value;
}

所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}

再看子类的两个重写的方法的类型:

@Override  
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}

父类和子类的参数类型不一样,这分明是重载,怎么可能是重写呢?

别着急,我们看一下测试:

public static void main(String[] args) throws ClassNotFoundException {  
DateInter dateInter=new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object());//编译错误
}

如果是重载,那么子类中的方法列表应该是这样的:

public void setValue(Object value)
public Object getValue()
public void setValue(Date value)
public Date getValue()

可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法(因为编译出错了)。

所以说,却是是重写了,而不是重载了。那么,真相究竟是什么呢?

原因是这样的,我们传入父类的泛型类型是Date和Pair< Date>,我们的本意是将Pair< Date>变为如下:

class Pair {  
private Date value;
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
}

然后再在子类中重写参数类型为Date的那两个方法,实现继承中的多态。

我们的本意是进行重写,实现多态。可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突

JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法啊。

于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>"
:()V
4: return

public void setValue(java.util.Date); //我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
5: return

public java.util.Date getValue(); //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn

public java.lang.Object getValue(); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法
;
4: areturn

public void setValue(java.lang.Object); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法
)V
8: return
}

从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。

不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。

setValue方法是为了解决类型擦除与多态之间的冲突。

而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:

那么父类的getValue方法如下:

public Object getValue() {  
return value;
}

而子类重写的方法是:

public Date getValue() {  
return super.getValue();
}

其实这在普通的类继承中也是普遍存在的重写,这就是协变。

还有一点也许会有疑问,子类中的桥方法 Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

3.2 泛型类型变量不能是基本数据类型

不能用类型参数替换基本类型。如,没有ArrayList< double>,只有ArrayList< Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

3.3 无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。

ArrayList<String> arrayList = new ArrayList<String>();

因为类型擦除之后,ArrayList< String>只剩下原始类型,泛型信息String不存在了。

if( arrayList instanceof ArrayList<String>) //错误
if( arrayList instanceof ArrayList<?>) //通过

3.4 泛型类并没有自己独有的Class类对象。

比如并不存在List< String>.class或是List< Integer>.class,而只有List.class。

静态变量是被泛型类的所有实例所共享的。对于声明为MyClass< T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass< String>还是new MyClass< Integer>创建的对象,都是共享一个静态变量。

3.5 异常中使用泛型的问题

(1)不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法。例如:下面的定义将不会通过编译:

public class Problem<T> extends Exception{......}

为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,在看下面的定义:

try{  
...
}catch(Problem<Integer> e1){
...
}catch(Problem<Number> e2){
...
}

类型信息被擦除后,那么两个地方的catch都变为原始类型Object,JVM无法对它们进行区分。即使是catch两个一模一样的普通异常,JVM也不会通过编译:

try{  //不能通过编译
...
}catch(Exception e1){
...
}catch(Exception e2){
...
}

(2)不能在catch子句中使用泛型变量

public static <T extends Throwable> void doWork(Class<T> t){  
try{
...
}catch(T e){ //编译错误
...
}
}

泛型信息在编译的时候已经变为原始类型Throwable,如果可以在catch子句中使用泛型变量,那么,下面的定义呢:

public static <T extends Throwable> void doWork(Class<T> t){  
try{
...
}catch(T e){ //编译错误
...
}catch(IndexOutOfBounds e){
}
}

根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。即使你在使用该静态方法的使用T是ArrayIndexOutofBounds,在编译之后还是会变成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子类,违背了异常捕获的原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。

(3)在异常声明中可以使用类型变量。
下面方法是合法的。

public static<T extends Throwable> void doWork(T t) throws T{  
try{
...
}catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}

3.5 不能创建泛型数组

解决方案:在任何想要创建泛型数组的地方使用ArrayList。
泛型类最好不要同数组一块使用。你只能创建new Pair< ?>[10]这样的数组,无法创建new Pair< String>[10]这样的。

public class Test {
private Pair<?>[] array1 = new Pair<?>[10]; //编译通过
private Pair<String>[] array2 = new Pair<String>[10]; //编译不通过

public static void main(String[] args) {
}
}

这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。

public class Test {
private List<Pair<String>> array3 = new ArrayList<Pair<String>>(); //编译通过

public static void main(String[] args) {
}
}

3.6 不能实例化泛型类型

T first = new T(); //ERROR  
T[] mm = new T[2]; //ERROR

可以用反射构造泛型对象和数组。利用反射,调用Array.newInstance:

public static <T extends Comparable> T[] minmax(T[] a)  {  
T[] mm == (T[])Array.newInstance(a.getClass().getComponentType(),2);
return (T[]) mm;
}

3.7 类型擦除后的冲突

(1)当泛型类型被擦除后,创建条件不能产生冲突。如果在Pair类中添加下面的equals方法:

class Pair<T>   {  
public boolean equals(T value) {
return null;
}
}

考虑一个Pair< String>。从概念上,它有两个equals方法:

booleanequals(String); //在Pair<T>中定义
boolean equals(Object); //从object中继承

但是,这只是一种错觉。实际上,擦除后方法boolean equals(T) 变成了方法 boolean equals(Object)

这与Object.equals方法是冲突的!当然,补救的办法是重新命名引发错误的方法。

(2)泛型规范说明提及另一个原则“要支持擦除的转换,需要强行制一个类或者类型变量不能同时成为两个接口的子类,而这两个子类是同一接口的不同参数化。”

class Calendar implements Comparable<Calendar>{ ... } 
class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...} //ERROR

GregorianCalendar会实现Comparable< Calender>和Compable< GregorianCalendar>,这是同一个接口的不同参数化实现。

这一限制与类型擦除的关系并不很明确。非泛型版本:

class Calendar implements Comparable{ ... }  
class GregorianCalendar extends Calendar implements Comparable{...} //合法

3.8 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

public class Test2<T> {    
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

但是要注意区分下面的一种情况:

public class Test2<T> {    
public static <T>T show(T one){//这是正确的
return null;
}
}

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。

参考:http://blog.csdn.net/lonelyroamer/article/details/7868820