无论是java、C#、kotlin还是scala,都会遇到泛型中的协变、逆变场景,算是泛型必学技。本文的概念、思想在其他三门语言也是通用的,学会包您一招通杀。
概念
假设
- A、B表示类型
- ≤ 表示继承关系
- f<⋅>表示类型转换
若A ≤ B,则 A是B的子类,B是A的超类
协变、逆变
什么是型变?型变(type variance)允许对类型进行子类型转换。
为了下面讲解先定义几个类:
static class Animal {}
static class Human extends Animal {}
static class Wolf extends Animal {}
static class Man extends Human {}
1. 协变(covariant)
当A≤ B有 f<A> ≤f <B>
List<? extends Animal> list=new ArrayList<Human>();
子类通配符(也叫上界通配符)<? extends T>
规定了元素的根类是Animal,可get出Animal类型
Animal animal=list.get(0);
但不能add,继承自Animal的类型可以是Human也可以Wolf,若添加Wolf肯定出错所以编译器禁止
list.add(new Human());
list.add(new Wolf());
java中的数组默认支持协变的
Animal []array=new Animal[3];
array[0]=new Human();
array[1]=new Wolf();
array[2]=new Man();
2. 逆变(contravariant)
当A≤B有 f<B>≤f<A>
List<? super Human> list=new ArrayList<Animal>();
超类通配符(也叫下界通配符)<? super T>
规定了元素的超类是Human,所以可以add Human类型及其子类型
list.add(new Human());
list.add(new Man());
不可以 get Human类型,因为没明确指定统一的“根”(除Object),只能get出Object类型
Human h=list.get(0);
3. 不变(invariant)
当A≤B时上述协变、逆变均不成立
List<Object> list=new ArrayList<String>();
Java的泛型默认是不变的(在不使用上界/下界通配符的情况下)
生产者协变,消费者逆变
哪么实际使用中何时协变何时逆变呢?《Effective Java》总结好了:producer-extends, consumer-super(PECS)。
- 读取(get)的对象称为生产者(Producer),可从生产者中安全地读取;
- 写入(add)的对象称为消费者(Consumer) ,可对消费者写入该类型和字类型(Human和Human的子类Man)。