这部分知识之前粗略看过,但没有一个整体性的测试和总结,今天在这里补上。
首先,这个事情是由继承搞出来的,既然源于继承,那我就先构造出祖孙三代。
//动物
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<Animal> animalList = new ArrayList<>();
List<Rabbit> rabbitList = new ArrayList<>();
List<DodgeRabbit> dodgeRabbitList = new ArrayList<>();
//报错,说明它们的房子之间没任何关系
//animalList = rabbitList;
//rabbitList = animalList;
那么想有关系得怎么写呢?java中提供了通配符这种语法。
先来写协变方式,协变方式使用?extend表示,如下:
//子代混居的房子
List<? extends Animal> animalDescendantList = new ArrayList<>();
List<? extends Rabbit> 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<? super Rabbit> rabbitAncestorList = new ArrayList<>();
List<? super DodgeRabbit> dodgeRabbitAncestorList = new ArrayList<>();
//逆变形式的赋值是反过来的,父类的祖先一定是子类的祖先
dodgeRabbitAncestorList = rabbitAncestorList;
//逆变形式取出的只能是类体系的顶级父类object,这点很好理解
Object dodgeRabbit = dodgeRabbitAncestorList.get(0);
观察以上示例可以发现:
如果想正常存取,就别用通配符,这样的集合之间是不变关系。
如果只想取,就用extends,这是协变。
为什么这种情况不能存?拿兔子举例,List<? extend 兔子>这种写法,它实际可能是兔子列表,也可能是道奇兔列表,也可能是纯种道奇兔列表,编译器仅从当前的这个list引用信息无法知道他具体是什么,如果它实际上是道奇兔列表,那往里存父类兔子类型就会出错,为了避免这种不确定的情况,索性直接禁止这种行为(这跟迭代器快速失败是一个原理)
如果想存,就用super,这是逆变。
为什么这种情况取出的东西只能用Object承接?因为下界表示和java的类型系统的共同作用结果。可能存放的是t和t的子类,那只有最终的基类Object能够表示。
PECS(Producer Extends,Consumer Super)
泛型类Container<T>如果是吃T,那他就是消费者,如果吐T,那它就是生产者,作为生产者则用extends,作为消费者则用super。
这个其实很好理解也很好记,首先extends无法set,自然不适用消费者的情况,而用来承接T的变量super T这样就能承接所有T和T的子类实例,代码里可以直接用T的所有特征。而生产者对外提供extends T的实例,接收方也可以直接用T的所有特征。