泛型是从jdk 1.5之后引入的,对于开发者而言,使用泛型能够写出更加自然的代码,对于编写一些通用的类尤为有用。想象一下,如果没有泛型的支持,我们在要实现一个Integer类型的链表时,不能直接复用系统的List泛型类的各种子类,而必须自己继承List实现一个专门存储Integer类型元素的链表,特别麻烦不说,当这种链表数量多了,我们代码中会有很多这种类型的链表类。
现在java中Collection泛型类的各种子类已经得到了广泛的使用,例如:
// 越野汽车集合
List vehicles = new ArrayList<>();
vehicles.add(new OffRoadVehicle());
vehicles.add(new OtherSUV());
vehicles.add(new HaFu());
...
OffRoadVehicle vehicle = vehicles.get(0);
...
我们声明一个越野汽车的List,然后就可以用它来保存各种属于越野汽车类型的汽车。这种使用方式最为普遍,并且能够解决大部分的问题。但是有时候遇到的情况可能会稍微复杂一些,例如,现在有一批哈弗汽车和其他类型的OtherSUV需要到维修中心进行保养,类似于下面这样:
List otherSuvs = new ArrayList<>();
List hafus = new ArrayList<>();
...
offRoadVehicleMaintenance(otherSuvs);
offRoadVehicleMaintenance(hafus);
如果要你来编写这个offRoadVehicleMaintenance你会怎么写?机智的你肯定不会用重载啦,当然不是说重载实现不了,而是我们可以使用更简单的方式:
public static void offRoadVehicleMaintenance(List<? extends OffRoadVehicle> list) {
// 这里写保养逻辑...
}
这里的”List<? extends OffRoadVehicle>”就表示,这个列表存储的元素是从OffRoadVehicle(越野汽车)派生而来的,因此OtherSUV的列表和哈弗汽车的列表都能够用它来表示。
与此对应的还有”List<? super OffRoadVehicle>”,它正好相反,表示这个列表存储的元素的类型都是OffRoadVehicle的父类。
于是很多同学看到这里就按捺不住了,随手敲了几行代码来测试:
List<? extends OffRoadVehicle> vehicles = new ArrayList();
vehicles.add(new OtherSUV()); // 这里报错了...
很不幸,你的代码报错了。WTF!这个List不能往里存东西,要你有什么用?!赶紧试试另外一个:
List<? super OffRoadVehicle> vehicles = new ArrayList();
vehicles.add(new OffRoadVehicle()); // 这里没问题了
OffRoadVehicle suv = vehicles.get(0); // 但是这里又报错了...
逗我玩?这里能往里添加但是又不能往外取了,什么鬼!先别发火,下面就来解释一下其中的原因。
上面为了举例说明的向”List<? extends OffRoadVehicle>”中添加元素和从”List<? super OffRoadVehicle> vehicles”中获取元素的使用方法实际上是违背了super和extends在泛型中的设计理念的。
那再来看看List<? extends OffRoadVehicle>,它表示的确实是指这个列表存储的元素是从OffRoadVehicle派生而来,但是它强调的不是泛型元素的特性,而是强调的泛型类的特性。怎么理解?如果强调的是泛型元素的特性的话,那么你就理所应当地认为这个List应该既能往里面存储,又能往外取数据才对呀,但是要真是这样,那它和List
public static void offRoadVehicleMaintenance(List<? extends OffRoadVehicle> list) {
for (OffRoadVehicle vehicle : list) {
System.out.println(vehicle.getDriveNum()); // 输出4,(假设越野汽车都是四驱的)
}
}
这个函数即能够使用List
同理,List<? super OffRoadVehicle>表示的是这个List能够表示所有的包含OffRoadVehicle父类型元素的List,因为这个List要么是List
/**
// 小汽车车道的初始化,可以是小汽车专用道
private static List<? super Sedan> sedanLane = new ArrayList();
// 还可以是一般的车道等
private static List<? super Sedan> sedanLane = new ArrayList();
**/
sedanLane.add(sedan); // 小汽车可以跑
sedanLane.add(cooper); // mini cooper也可以跑
sedanLane.add(truck); // 但是大货车不能跑
上面使用sedanLane来表示小汽车能够跑的道,至于这个车道是什么,可以是小汽车专用车道,也可以是一般的普通车道。在这个车道上凡是属于小汽车的(派生于Sedan)都是可以跑的(add不出错),但是大货车可不行(Trck不是Sedan的子类)。
super和extends在Collection泛型中的特性可以使用PECS规则来总结,PECS是”Producer Extends, Consumer Super”的缩写,它简单表述如下:
PECS规则背后有其更深层次的原因,上面我们已经说过,将List
List<? extends OffRoadVehicle> offRoadVehicles;
List otherSUVList = new ArrayList<>();
otherSUVList.add(new OtherSUV());
offRoadVehicles = otherSUVList;
offRoadVehicles.add(new HaFu()); // 这里...
但是事实上,这明显是违反常理的。因为OtherSUV指的就是除哈弗之外的其他SUV,如果允许我们向List<? extends OffRoadVehicle>中添加数据,那么就通过offRoadVehicles间接地向otherSUVList添加了一个哈弗的对象,破坏了数据结构的约定。因此用extends约定的Collection是不能add元素的。
再来分析super,还是以上节车道的代码为例,List<? super Sedan>既能表示ArrayList
上面从类型冲突的方面来解释了为什么存在PECS规则的限制。实际上,这一切的最根本原因就是java泛型的边界擦除机制造成的。java的泛型和其他语言类型的泛型不一样,它实际上是一种伪泛型的实现,即在运行态,jvm是不会感知到泛型类型存在的,泛型类型的边界擦除、转换和补偿实际上都是由编译器完成的。例如
public class CommonGeneric {
private T mObj;
public void set(T genericObj) {
mObj = genericObj;
}
public void process() {
}
public T get() {
return mObj;
}
}
CommonGeneric类在运行时内部会直接将mObj当做一个Object对象来对待,这是因为在调用set将这个对象传递进来的时候编译器将genericObj类型信息擦除了,同时在get方法返回的时候编译器又生成了代码将擦除了类型信息的mObj对象转换成了指定的T类型。
边界擦除的一个副作用就是在CommonGeneric内部无法使用类型T的公共方法,例如,
CommonGeneric commonStr = new CommonGeneric<>();
commonStr.set(new Integer(0));
使用上面的代码实际上将一个Number类型设置为了CommonGeneric中的mObj成员,但是,在CommonGeneric内部我们仍然没有办法调用Number的公共方法,因为在CommonGeneric内部看来,T类型就是和Object类型是一样的。
为了在CommonGeneric内部保留部分泛型类型信息,我们可以使用下面的方法来改写CommonGeneric类:
public class CommonGeneric {
private T mObj;
public void set(T genericObj) {
mObj = genericObj;
}
public void process() {"
System.out.println(mObj.intValue());
}
public T get() {
return mObj;
}
}
在这里使用了extends来声明了泛型的上界,这样编译器在进行边界擦除的时候会擦除到T类型继承体系中最近的Number,即在CommonGeneric内部,T不再是Object类型,而是被当做一个Number类型,这样就能够在内部调用Number类型的公共方法(例如intValue()方法)了。
关于边界擦除和泛型的其他特性《Java编程思想》中还有进一步的讲解,这里就不再多讲。
由于CSDN markdown编辑器的原因,博客中代码的”?”号均为中文问号,但实际上应该是英文的问号”?”。