关于java泛型中List<? extends Base>的疑问?

2023-06-27 378 0

现在有代码如下,Sub是Base的子类:

public class Base {
}
public class Sub extends Base{
}

我能理解下面的代码:

    // 相当于list2的泛型是Base或者Base的超类,
    // 所以,list2中可以放Base或者Base的子类,因为list2的泛型如果是Base,那显然可以添加Base的实例;
    // Base的泛型如果是其超类,那显然,Base的任意子类都能向上转型为Base的超类
    List<? super Base> list1 = new ArrayList<>();
    list1.add(new Sub());
    list1.add(new Base());

现在的问题是,为什么不能调用list2的add方法添加,即便是添加Base实例也不行?

List<? extends Base> list2 = new ArrayList<>();

如果按照上面的理解,list2的泛型可以是Base或者Base的子类,如果说子类之间不能互相转型,那么,至少可以添加Base的实例吧?或者说,添加Base的子类也能转为Base类型,为什么java不允许这么做呢?

题主理解的是错误的:

  • extends 可用于返回类型限定,不能用于参数类型限定(换句话说:? extends xxx 只能用于方法返回类型限定,jdk能够确定此类的最小继承边界为xxx,只要是这个类的父类都能接收,但是传入参数无法确定具体类型,只能接受null的传入)。
  • super 可用于参数类型限定,不能用于返回类型限定(换句话说:? supper xxx 只能用于方法传参,因为jdk能够确定传入为xxx的子类,返回只能用Object类接收)。

详细:

https://zhuanlan.zhihu.com/p/267335337

因为 List<Base> 表示 里面是 Base 类型的对象
但是 List<? extends Base> 表示 这是一个 List<Base> 或者 List<Sub>
因此无法判断到底能放进去什么

这里有一个比较全的例子

class Scratch {
    public static void main(String[] args) {
        // 类型参数是一个确定的类型的时候,读写操作都可以执行
        Bag<Fruit> fruitBag = new Bag<>();
        fruitBag.set(new Apple());
        fruitBag.set(new Pear());
        Fruit fruit = fruitBag.get();
        Bag<Apple> appleBag = new Bag<>();
        Bag<Pear> pearBag = new Bag<>();
        // 这里会报错,因为 Bag<Fruit> 和 Bat<Apple> Bag<Pear> 类型不兼容
        Bag<Fruit> badBag1 = appleBag;
        Bag<Fruit> badBag2 = pearBag;
        // 这里可以通过,因为 Bag<? extend Fruit> 和 Bag<Apple> 和 Bag<Pear> 类型兼容
        Bag<? extends Fruit> appleFruitBag = appleBag;
        Bag<? extends Fruit> pearFruitBag = pearBag;
        // extends 的情况,只能'读'不能'写'
        // 因为 Bag<? extend Fruit> 可能是 Bag<Apple> 也可能是<Pear>
        // 所以无法确定能塞进去什么
        appleFruitBag.set(new Apple());
        appleFruitBag.set(new Pear());
        pearFruitBag.set(new Apple());
        pearFruitBag.set(new Pear());
        // 但是无论是什么,都是 Fruit,所以可以读取出来
        Fruit fruit1 = appleFruitBag.get();
        Fruit fruit2 = pearFruitBag.get();
        // super 的情况,只能'写'不能'读'(只能读出 Object)
        Bag<Object> objectBag = new Bag<>();
        Bag<? super Apple> superAppleBag1 = fruitBag;
        Bag<? super Apple> superAppleBag2 = objectBag;
        Bag<? super Apple> superAppleBag3 = appleBag;
        // 因为类型范围都比 Apple 大,所以可以放 Apple 进去
        superAppleBag1.set(new Apple());
        superAppleBag2.set(new Apple());
        // 因为不知道具体的类型是什么,因此也不能放父类对象进去
        superAppleBag2.set(new Fruit());
        // 同样因为不知道具体类型是什么,只能读 Object 类型出来
        Object o = superAppleBag2.get();
    }
    public static class Bag<T> {
        public void set(T t) {
        }
        public T get() {
            return null;
        }
    }
    public static class Fruit {
    }
    public static class Apple extends Fruit {
    }
    public static class Pear extends Fruit {
    }
}

extends 和 super 这两个关键词使用场景不一样:

  • 对于方法传递参数时: 对于一个集合类型,super T 对应放入生产场景(add T 的子类), extends 对应取出消费场景( 调用 T 的方法) ,

举个例子:

import java.util.ArrayList;
import java.util.List;
public class GenericTest {
    static class GrandParent{
        public void say(){
            System.out.println("GrandParent");
        }
    }
    static class Parent extends GrandParent{
        public void say(){
            System.out.println("Parent");
        }
        public void otherSay(){
            System.out.println("other from Parent");
        }
    }
    static class Child extends Parent{
        public void say(){
            System.out.println("Child");
        }
        public void otherSay(){
            System.out.println("other from Child");
        }
        public void childSay(){
            System.out.println("childSay");
        }
    }
    public static void say(List<? extends Parent> p){
        for(GrandParent g:p){
            g.say();
        }
        for(Parent g:p){
            g.otherSay();
        }
//        for(Child g:p){ // compile error
//            g.childSay();
//        }
//        p.add(new Child()); // 消费者不能添加元素
    }
    public static void add(List<? super Parent> p){
        p.add(new Child());
        p.add(new Parent());
        // p.add(new GrandParent()); // compile error
        // Parent c = p.get(0); // 生产者不能获取元素
    }
    public static void main(String[] args) {
        List<Parent> family = new ArrayList<>(); // 生产者同时也是消费者,则只能使用明确的类型
        add(family);
        say(family);
        List<Child> family2 = new ArrayList<>();
//        add(family2); // compile error
        say(family2);
        List<GrandParent> family3 = new ArrayList<>();
        add(family3);
//        say(family3); // compile error
        List<? extends Parent > family4 = new ArrayList<>();
//        add(family4); // compile error
        say(family4);
        List<? super Parent> family5 = new ArrayList<>();
        add(family5);
//        say(family5); // compile error
    }
}

看这里: https://stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java

泛型可以理解为一种编译约束,能够在编译阶段找出来所有违反约束的问题,它是声明式的
约束就会有很多预先定义好的规则,你这里遇到的extends和super的问题,是泛型里面的PECS规则限制的
PECS = Producer Extends,Consumer Super
当定义泛型容器Collection<?> 的时候,
如果希望容器是个生产者,使用extends,生产型容器的约束是:我能够提供汽车轮胎(Tier),但可能来自任意品牌,你可以读取我,获取轮胎,自行判断是不是你需要的品牌,但是我不接收轮胎,我的职责是单一的,只读不存
如果希望容器是个消费者,使用Super,消费型容器的约束是:我能够存放轮胎,你可以添加任意品牌的轮胎,但是我不提供轮胎,我的单一职责,只存不读

这个规则的本质是:面对对象是支持型变的(子类 is a 父类),但泛型作为一种对象类型的安全性约束,本身是不支持型变的
所以如果单纯定义List<Parent>和List<Child>,两者是没有任何继承关系的,但有的时候我们又希望能够约束容器中元素的型变范围,所以大神们就发明使用了extends/super,这里的有的时候指的就是生产或者消费场景。
再进一步,抛开容器泛型的使用场景和PECS,泛型的extends/super本身涉及到概念叫协变(Covariance)和逆变(Contravariance),挺难一下子解释清楚的两个概念,可以多搜一些资料,了解一下Fruit和Apple的故事...

总结一下,遇到使用容器泛型的场景,又希望约束下容器支持的行为,那只读不存用extend,只存不读用super,不约束就只用问号,这个一般通常配合函数来使用,约束函数的使用场景,提供安全性。
举例:Collections提供的copy方法,src只读不存,dest只存不读

    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());
            }
        }
    }

回答

相关文章

nuxt2部署静态化和ssr的时候访问首页先报404再出现首页为什么?
`clip-path` 如何绘制圆角平行四边形呢?
多线程wait方法报错?
VUE 绑定的方法如何直接使用外部函数?
vue2固定定位该怎么做?
谁有redis实现信号量的代码,希望借鉴一下?