从extends和super浅谈java泛型

从extends和super浅谈java泛型

泛型是从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); // 但是这里又报错了...

逗我玩?这里能往里添加但是又不能往外取了,什么鬼!先别发火,下面就来解释一下其中的原因。

泛型中的super和extends

上面为了举例说明的向”List<? extends OffRoadVehicle>”中添加元素和从”List<? super OffRoadVehicle> vehicles”中获取元素的使用方法实际上是违背了super和extends在泛型中的设计理念的。

  • “? extends OffRoadVehicle”表示的是泛型元素是从OffRoadVehicle派生而来(例如OtherSUV和HaFu),指明的是泛型元素的上限为OffRoadVehicle。
  • “? super OffRoadVehicle”表示泛型元素的类型应该是OffRoadVeicle的父类(例如Car或者Vehicle),指明的是泛型元素的下限为OffRoadVehicle。

那再来看看List<? extends OffRoadVehicle>,它表示的确实是指这个列表存储的元素是从OffRoadVehicle派生而来,但是它强调的不是泛型元素的特性,而是强调的泛型类的特性。怎么理解?如果强调的是泛型元素的特性的话,那么你就理所应当地认为这个List应该既能往里面存储,又能往外取数据才对呀,但是要真是这样,那它和List又有什么区别?是吧。所以,List<? extends OffRoadVehicle>强调的是泛型类List在这里的特性,它能够表示所有的包含OffRoadVehicle派生类型元素的List。也许解释得有点绕,但是看看上面的那个汽车保养的函数你也许就能够理解了:

public static void offRoadVehicleMaintenance(List<? extends OffRoadVehicle> list) {
    for (OffRoadVehicle vehicle : list) {
        System.out.println(vehicle.getDriveNum());      // 输出4,(假设越野汽车都是四驱的)
    }

}

这个函数即能够使用List做参数,又能够使用List做参数,原因就是它的形参List<? extends OffRoadVehicle>正好都能够表示这两种类型,这也就是extends在java泛型中的应用之一。
同理,List<? super OffRoadVehicle>表示的是这个List能够表示所有的包含OffRoadVehicle父类型元素的List,因为这个List要么是List要么是List,要么是List,甚至是List,所以我们可以放心地向其中添加OffRoadVehicle或者其子类的对象。至于使用场景,可以简单地考虑:

/**
// 小汽车车道的初始化,可以是小汽车专用道
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的子类)。

PECS规则

super和extends在Collection泛型中的特性可以使用PECS规则来总结,PECS是”Producer Extends, Consumer Super”的缩写,它简单表述如下:

  • 如果想从Collection中获取一个T类型的元素(Collection生产一个T类型元素),那么需要声明这个Collection为”? extends T”,例如List<? extends T>,从这个Collection获取的元素能够保证一定是T或者T的子类的对象,但是不能向这个Collection中添加元素。
  • 如果想把一个T类型元素加入到Collection中(Collection消费一个T类型的元素),那么需要声明这个Collection为”? super T”,例如List<? super T>,但是从这个Collection中获取的元素却不能保证是什么类型的(因此不能从这个Collection中获取元素)。
  • 如果想对Collection进行读写,那么还是不要使用super和extends,直接指定类型吧。

PECS规则背后有其更深层次的原因,上面我们已经说过,将List和List赋值给List<? extends OffRoadVehicle>都是合法的,假设允许向List<? extends OffRoadVehicle>中add元素,那么下面的代码应该是可以通过编译的:

    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()又能表示ArrayList,因此我们可以安全地向List<? super Sedan>中添加Sedan的任何子类,但是如果允许从List<? super Sedan>中获取元素,那么这个类型是Sedan还是Automobile?这点是无法确定的,例如,我们如果认为取出的元素是Sedan类型的,但是向List<? super Sedan>添加Automobile又是合法的,但是Automobile并不是Sedan类型,而是它的父类,这样就产生了类型冲突,因此,使用super来标志的类型下限的Collection是不能从其中获取元素的。

边界擦除

上面从类型冲突的方面来解释了为什么存在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编辑器的原因,博客中代码的”?”号均为中文问号,但实际上应该是英文的问号”?”。

参考:

  • https://stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java
  • https://segmentfault.com/a/1190000005179142

你可能感兴趣的:(java)