Java进阶系列-协变逆变与泛型

MSDN的dotNET文档中有对协变逆变的定义,截取下来

协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。 泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。

泛型这东西特别是通配符那边确实有点绕,不好理解。

Java中数组是协变的,因为它可以这样

1
Number[] num = new Integer[10];

结合最上面的定义,再想想。

但Java的泛型匹配本身前后没有继承关系,既没有协变性也没有逆变性,是不变的关系

所以你不能这样

1
List<Number> num = new ArrayList<Integer>();

更不能这样

1
List<Integer> num = new ArrayList<Number>();

但是有办法实现泛型的协变和逆变,就是使用泛型通配符

1
2
3
4
// 实现泛型协变
List<? extends Number> test = new ArrayList<Integer>();
// 实现泛型逆变
List<? super Number> test1 = new ArrayList<Object>();

这两个List遵循PECS(producer-extends, consumer-super)原则,生产者List使用extends,消费者List使用super。

换句话说,就是上面的test集合add方法受限,get方法不受限但只能用Number类型接收;test1集合可以add,但get方法受限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.ArrayList;
import java.util.List;

public class Main {

public static void main(String[] args) {
List<? super Number> list = new ArrayList<>();
List<? extends Number> list1 = new ArrayList<>();
list.add(Integer.valueOf(1)); // 通过
Integer a = list.get(0); // 编译错误
Object a = list.get(0); // 通过
list1.add(Integer); // 编译错误
list1.add(null); // 通过
Number b = list1.get(0); // 通过
}
}

我写这个就是为了记一下这两个到底为什么,说实话有点不好理解,网上说法很多,也说不到点,只能按照自己理解了。

一句话,泛型只存在编译阶段,是给编译器看的,编译通过后就没泛型这个东西了,一切只为类型安全,防止意外。

<? extends E>适合只用来get的场合,属于生产者,为什么不能add。

我们假设现在有A,B,C,D四个类,它们的关系有

1
2
3
4
class A {}
class B extends A {}
class C extends B {}
class D extends C {}

extends用来定义泛型上界,这里表示E和所有E的子类范围,现在我定义一个List

1
2
3
List<? extends B> list = new ArrayList<C>(); // 正确
List<? extends B> list = new ArrayList<D>(); // 正确
List<? extends B> list = new ArrayList<A>(); // 编译错误

可以看到只要是B或B的子类的ArrayList都能声明。假如我定义的是List<? extends B> list = new ArrayList<D>();,编译器又允许add的话,那我add一个C类型的元素进去怎么办,D类型的怎么去接收他的父类C类型呢?这时ArrayList就没办法安全的接收元素了,而且编译器其实在有些时候没法确定ArrayList的泛型,比如在函数形参上,void function(List<? extends B> list);,你没调用这个函数时编译器根本不知道你会给他的实参是什么,所以编译器干脆统一不让add了(除了null,因为null代表任何类型),但是get是很安全的,因为至少可以保证所有元素都是B或B的子类,所以里面的元素都统一当作B类型,这样get出的元素只能用B类型的变量来接收。

<? super E>适合只用来add的场合,属于消费者。

super用来定义泛型下界,这里表示E和所有E的基类范围

1
2
3
List<? super C> list = new ArrayList<B>(); // 正确
List<? super C> list = new ArrayList<A>(); // 正确
List<? super C> list = new ArrayList<D>(); // 编译错误

他可以add的范围则是E和E的所有子类,为了类型安全E的父类不会被允许add,比如我这样定义List<? super C> list = new ArrayList<B>();,那么我规定只能add进C或C的子类,这样才能保证ArrayList能安全接收,因为ArrayList的泛型范围是[C , ?),肯定可以安全接收处在(? , C]类型范围类的元素,所add是被允许的,但是get受限,这样B value = list.get(0);,这样C value = list.get(0);都不行,只能Object value = list.get(0);

get方法由于类型丢失,所以get出来的元素除了Object类型以外都不能接受(这里不谈强制类型转换,因为这里通配泛型就是为了能扩大接收的类型范围,加入的元素类型是不确定的,如果都确定是某个类型了比如String了,那干嘛不直接声明List呢)。

那一个不能正常add一个不能正常get要他有什么用?比方用来当作函数参数,接收数据,并规定类型范围(不像<?>什么都接收)

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.ArrayList;
import java.util.List;

public class Main {

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
List<? extends Number> list1 = list;
list1.forEach(System.out::println);
}
}
文章作者: Shawn Qin
文章链接: https://qinshuang1998.github.io/2019/02/11/java-progress-02/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Shawn's Blog