不变、协变和逆变

这部分知识之前粗略看过,但没有一个整体性的测试和总结,今天在这里补上。

首先,这个事情是由继承搞出来的,既然源于继承,那我就先构造出祖孙三代。

//动物
class Animal{
    private final String speciesName;
    private final String name;

    public Animal(String speciesName, String name) {
        this.speciesName = speciesName;
        this.name = name;
    }

    public String getSpeciesName() {
        return speciesName;
    }

    public String getName() {
        return name;
    }

    void move(){
        throw new UnsupportedOperationException("无具体类型");
    }
}
//兔子
class Rabbit extends Animal{
    public Rabbit(String name) {
        super("兔子", name);
    }

    @Override
    void move() {
        System.out.println(getSpeciesName() + getName() + "蹦蹦跳跳地移动");
    }
}
//道奇兔
class DodgeRabbit extends Rabbit{
    public DodgeRabbit(String name) {
        super(name);
    }
}

然后描述一下这个伦理问题:
我是你的儿子,那我的房子跟你的房子是什么关系?
A.没关系:这就是不变。
B.我的房子是你的房子的儿子:这就是协变。
C.我的房子是你的房子的爸爸:这就是逆变。

那么在代码里是什么情况呢?如下:

        //先搞出祖孙三代
        Animal animal = new Animal("动物", "phantom");
        Rabbit robbie = new Rabbit("robbie");
        DodgeRabbit dodge = new DodgeRabbit("dodge");
        //再搞出它们的房子
        List animalList = new ArrayList<>();
        List rabbitList = new ArrayList<>();
        List dodgeRabbitList = new ArrayList<>();
        //报错,说明它们的房子之间没任何关系
        //animalList = rabbitList;
        //rabbitList = animalList;

那么想有关系得怎么写呢?java中提供了通配符这种语法。
先来写协变方式,协变方式使用?extend表示,如下:

        //子代混居的房子
        List animalDescendantList = new ArrayList<>();
        List rabbitDescendantList = new ArrayList<>();
        //这样就没问题
        animalDescendantList = rabbitDescendantList;
        //x子代协变取出的就是x类型
        Animal ad = animalDescendantList.get(0);
        Rabbit rd = rabbitDescendantList.get(0);
        //报错,使用通配符定义的无法添加具体对象
        //animalDescendantList.add(animal);
        //能添加null,但是没什么意义
        animalDescendantList.add(null);

再来逆变方式:

        List rabbitAncestorList = new ArrayList<>();
        List dodgeRabbitAncestorList = new ArrayList<>();
        //逆变形式的赋值是反过来的,父类的祖先一定是子类的祖先
        dodgeRabbitAncestorList = rabbitAncestorList;
        //逆变形式取出的只能是类体系的顶级父类object,这点很好理解
        Object dodgeRabbit = dodgeRabbitAncestorList.get(0);

观察以上示例可以发现:

如果想正常存取,就别用通配符,这样的集合之间是不变关系。

如果只想取,就用extends,这是协变。

为什么这种情况不能存?拿兔子举例,List这种写法,它实际可能是兔子列表,也可能是道奇兔列表,也可能是纯种道奇兔列表,编译器仅从当前的这个list引用信息无法知道他具体是什么,如果它实际上是道奇兔列表,那往里存父类兔子类型就会出错,为了避免这种不确定的情况,索性直接禁止这种行为(这跟迭代器快速失败是一个原理)

如果想存,就用super,这是逆变。

为什么这种情况取出的东西只能用Object承接?因为下界表示和java的类型系统的共同作用结果。可能存放的是t和t的子类,那只有最终的基类Object能够表示。

PECS(Producer Extends,Consumer Super)

泛型类Container如果是吃T,那他就是消费者,如果吐T,那它就是生产者,作为生产者则用extends,作为消费者则用super。
这个其实很好理解也很好记,首先extends无法set,自然不适用消费者的情况,而用来承接T的变量super T这样就能承接所有T和T的子类实例,代码里可以直接用T的所有特征。而生产者对外提供extends T的实例,接收方也可以直接用T的所有特征。

你可能感兴趣的:(不变、协变和逆变)