美文网首页
java泛型之通配符?

java泛型之通配符?

作者: 逝去丶浅秋 | 来源:发表于2020-10-02 17:30 被阅读0次

    一、在说泛型通配符" ?" 之前先讲几个概念

    1、里氏替换原则(Liskov Substitution Principle, LSP):

    定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

    LSP包含以下四层含义:

    • 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
    • 子类中可以增加自己的方法。
    • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
    • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

    因为继承带来的侵入性,增加了耦合性,也降低了代码的灵活性。父类修改了代码,子类可能也会受到影响,为了减少这些影响,我们就需要里氏替换原则,虽然不遵循里氏替换原则,程序照样可以运行,但是出错的几率会大大增加。

    任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用。

    2、逆变、协变和不可变:

    定义:逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

    • A≤B时,如果有f(A)≤f(B)成立,那么f(⋅)是协变(covariant)的;
    • A≤B时,如果有f(B)≤f(A)成立,那么f(⋅)是逆变(contravariant)的;
    • A≤B时上述两个式子均不成立,即f(A)f(B)相互之间没有继承关系,那么f(⋅)是不变(invariant)的;
    3、数组的协变与逆变:

    1、基本类型数组不允许协变和逆变,无法通过编译。
    2、引用类型数组允许协变和逆变,逆变时会检查实际类型,如果不符抛出java.lang.ClassCastException
    下面看代码:

    //继承关系如下
    class Fruit {}
    class Orange extends Fruit {}
    class Apple extends Fruit {}
    class RedApple extends Apple {}
    
    //数组的协变:Apple是Fruit的子类,Apple数组可以赋值给Fruit数组
    Fruit[] fruits = new Apple[10]; 
    fruits[0] = new Apple();
    fruits[1] = new RedApple();
    //下面两个会编译可以通过,但是在运行时会抛出java.lang.ArrayStoreException
    //因为虽然定义了Fruit[],但实际上指向的是Apple[]
    fruits[2] = new Fruit();
    fruits[3] = new Orange();
    
    //数组的逆变:编译时可以通过,但是运行时会抛出java.lang.ClassCastException
    Apple[] apples = (Apple[]) new Fruit[10];
    

    java中数组的协变是个坑,需要注意一下,因为在编译期它允许放入FruitOrange等非法类型,但是在运行时还是会出现类型错误。

    4、先来说一下泛型的不可变:
    List<Fruit> fruitList = new ArrayList<Apple>();//报错
    

    上面的代码编译期会直接报错,类比数组,泛型将这种类型检查移到了编译期,所以说泛型是不可变的,但有时我们需要泛型可以协变和逆变,那么该怎么办呢?

    解决的办法就是下面要讲的<? extends T>(协变)和<? super T>(逆变),也就是平常说的泛型的上界和下界。

    5、泛型的 PECS (Producer Extends Consumer Super)原则:

    生产者(Producer)使用extends,消费者(Consumer)使用super

    • 经常往外读取内容的,适用于上界Extends
    • 经常往里插入的,适用于下界Super

    二、泛型<? extends T>上界与<? super T> 下界

    • <? extends T> 表示参数化类型的上界,表示参数化类型可能是T 或是T 的子类;
    • <? super T> 表示参数化类型下界(Java Core中叫超类型限定),表示参数化类型是TT 的超类型(父类型),直至Object;

    先来讲一下参数化类型和通配符<?>的区别:

      (1)参数化类型T,它指代的是同一个类型,比如下面的三个T都指代用一个类型,要么是String,要么是其他的,但这三个T都必须是同一个类型。

    public <T> List<T> fill(T... t );
    

      (2)而通配符<?>没有这种约束,List<?>单纯的表示,集合里放了一个东西,是什么我不知道。

    1、<? extends T>上界:
    List<? extends T> list = new ArrayList<可以是T或是T的子类>();
    

    <? extends T>实现了泛型的协变。当A≤B时,有f(A)≤f(B)成立。看下面代码:

    List<? extends Fruit> fruitExtends = new ArrayList<>();
    List<Fruit> fruits = new ArrayList<>();
    List<Apple> apples = new ArrayList<>();
    
    fruits = apples;    //这个是报错的
    fruitExtends = apples;  //List<? extends Fruit>使其实现了协变,不报错
    

    上面代码可以看出,可以把Fruit及其子类的ArrayList赋值给List<? extends Fruit> fruits。实现了泛型的协变。

    在PECS原则中,extends代表生产者,特点是:不能往里存,只能往外取

    List<? extends Fruit> fruits = new ArrayList<>();
    fruits.add(new Apple());//报错
    fruits.add(new RedApple());//报错
    fruits.add(null);//不报错,虽然不知道fruits所持有的具体元素,但null代表任何类型
    

    (1)不能往里存的原因,如上代码表示list里的元素只能是FruitFruit的子类,因为无法确定List所持有的具体的类型是什么,只知道里面是任何一个继承了Fruit 的子类,所以无法向其中添加元素。编译器在看到apples的赋值,但是List<? extends Fruit>并没有标明其中就是Apple,而是用上了一个占位符capture#1来表示捕获了一个FruitFruit的子类,其实它并不知道是什么,你的插入都和capture#1不匹配。如果可以往里存,那么对于fruits来说,你可以往里放一个Apple,也可以放一个Orange,但这显然是不对的。

    需要注意的是:可以添加null,因为null可以表示任何类型。

    fruit.add(null);

    (2)只能往外取的原因<? extends T>指定了T为所有元素的“根”,所以可以用T来使用容器里的元素。也就是说不管是什么子类,不管追溯多少辈,肯定有个父类T,所以,对于get方法,我们可以用最大的父类T来接着,也就是把所有的子类向上转型为T

    2、<? super T>下界:
    List<? super T> list = new ArrayList<可以是T或是T的父类>();
    

    <? super T>实现了泛型的逆变。当A≤B时,有f(B)≤f(A)成立。看下面代码:

    List<? super Apple> appleSuper = new ArrayList<>();
    List<Fruit> fruits = new ArrayList<>();
    List<Apple> apples = new ArrayList<>();
    
    apples = fruits;    //这个是报错的
    appleSuper = fruits;    //List<? super Apple>使其实现了逆变,不报错
    

    上面的代码可以看出,可以把Apple的父类赋值给List<? super Apple> appleSuper。实现了泛型的逆变。

    在PECS原则中,super代表消费者,特点是:只能往里存,不能往外取。

    (1)不能往外取的原因,和extends一样,list里存的只能是TT的父类,因为无法确定具体的类型是什么,所以往外取时并不知道取出来的是什么,所以无法从list中取元素。

    需要注意的是:因为所以的对象都有个必然的父类Object,所以可以读取到Object对象。和extends的null一样。

    Object obj = list.get(n);

    appleSuper.add(new Apple());
    appleSuper.add(new RedApple());
    appleSuper.add(new Fruit());    //报错
    

    (2)只能往里存的原因<? super T>指明了集合中存放的只能是TT的父类,所以我们可以把fruits赋值给appleSuper,往里面添加元素时,上面的代码可以看出,往集合里添加AppleApple的子类时是可以的,但是存Apple的父类时就会报错。

    这里可能会有一个疑问,集合中存放的是AppleApple的父类,我们将其父类添加进去的时候为什么会报错呢?

    虽然这是一个AppleApple的父类的容器,但是具体的是什么类型并不知道,而往里存Apple的父类时,可能往里存的是一个Friut或是一个和Fruit无关的接口,那么就出现了两个不想关的对象。往里添加AppleApple的子类时,不关这个子类是什么,它都有一个共同的父,所以是可以的。

    三、泛型通配符该什么时候使用?

    Effective Jave一书中总结:为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果每个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型比配,这是不用任何通配符而得到的。

    简单来说就是PECS原则。

    一个经典的例子是java.uitl.Collections中的copy()方法:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");
    
        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }
    

    src作为生产者只从其中取数据,dest作为消费者,只往里存放数据。

    相关文章

      网友评论

          本文标题:java泛型之通配符?

          本文链接:https://www.haomeiwen.com/subject/vcgbuktx.html