java的协变与逆变
在日常的开发中,你是否经常看见List<?>、List<T>、 List<Object>、List<? extends Number>、List<? super Integer>等形式的泛型定义。当你对这几种类型不了解的时候也就无法理解逆变与协变。当然,逆变与协变的产生本质上还是由于Java的多态。
首先,来了解下以上讲的几种泛型。注意:本文用集合的泛型来解释说明。
List<?>:表示存放一种未知的特定类型的集合。这种一般只能读取数据,而不能写入数据,可读是因为不管集合存放什么类型的数据,该类一定是继承自Object的,而不能写是因为集合存放的是特定的数据类型,但是编译器又不知道具体的类型,因此无法向其写入数据。比如
List<?> list = new ArrayList<Integer>();
list = new ArrayList<String>();
list.add(new Object())//编译出错,实际存放的可能是Number类型之类
Object o = list.get(0);//编译正常
List<T>:表示存放一种已知的特定类型的集合。为什么说是已知,因为在实际使用的时候,要将T替换成实际的类型,而不能像List<?>这样直接使用,当然了,既然是确定的类型,就可进行读写。比如:
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.get(0);
List<Object>:表示集合存放的是Object类型的数据,可对List<Object>进行读写,可能有人会将其与List<?>混淆了。我觉得可这样理解,List<?>表示编译器不知道存放的是什么类型,因此可能是List<Integer>,也可能是List<String>,因此你往List<?>写入int不合适,写入String也不合适,写入Object类型更不行(无法将父类的对象赋予子类的引用)。但是无论是Integer还是String,读取出来的类型一定的Object类型的子类。因此可对List<?>读取。而List<Object>,已经明确告诉编译器List存放的Object类型的,因此可以向List<Object>写入和读取。
List<? extends Number>和List<? super Integer>的泛型类型就是本文要讲的协变与逆变。在讲这个概念前,再回忆下Java的多态,父类的引用可指向子类的对象。比如Fruit fruit = new Apple();注意:fruit的静态类型是Fruit,实际类型是Apple。
那么何为协变与逆变?
假如现在有两种类型:P和C,P是C的父类,根据多态可知P类型的引用可指向C类型的对象。此时用F(X)表示基于P和C的其他类型,如List<P>和List<C>。
协变:f(P)是f(C)的父类,f(p)的引用可指向f(C)的对象,此时称为协变。
逆变: f(P)是f(C)的子类,f(C)的引用可指向f(P)的对象,此时称为逆变。
不变: f(P)与f(C)不是父子关系。
在了解了逆变与协变的定义后,再用实际的例子来说明下。我们知道Number是Integer的父类型,但是List<Number>是List<Integer>的父类么,答案明显不是的。因为List<Number> list = new ArrayList<Integer>()无法成立。也就是List<Number>与List<Integer>是不变的。为什么List<Number>引用无法指向List<Integer>对象?可以先假设为可以,看以下代码:
List<Number> list2 = new ArrayList<Integer>();//编译出错
list2.add(1.2);
Integer number = (Integer) list2.get(0);
list2是List<Number>的引用,因此可以往list2添加任何Number类型及其子类的数据,这时我们加入一个double类型的。但是,又由于list2实际指向的是List<Integer>对象,因此从list2取出来的数据,根据泛型可知一定是Integer类型,因此对其强制类型转换,这就与加入时double类型相矛盾了,因此编译器也不允许我们这样做。
下面就真正的解释协变与逆变了。也就是本文一开始就提到的List<? extends Number>和List<? super Integer>。还是看下面例子:
List<? extends Number> list3 = new ArrayList<Integer>();//编译通过
list3.add(1);//编译报错
Number number1 = list3.get(0);//编译通过
通过上述例子可知,List<? extends Number>的引用可指向List<Integer>的对象,因此说明List<? extends Number>是协变的。但是只能对协变的类型进行读取而不能写入。首先List<? extends Number>规定了集合类型的上界为Number类型的,但是并没有说明具体的类型,可能是Integer类型,也可能是Double类型,因此无法对其进行写入。但是可以取是因为,无论集合存放的是什么类型,取出来的一定是Number类型的。有人可能会说,那我们不是把List<Integer>赋值给它了么,为什么不能写入Integer类型的数据?这边在提醒下,在编译期时检查的是集合的静态类型List<? extends Number>,而不是实际类型List<Integer>。再来看下另一个说明逆变的例子,如下:
List<? super Integer> list4 = new ArrayList<Number>();//编译通过
list4.add(1);//编译通过
Integer integer = list4.get(0);//编译出错
通过上述例子可知,List<? super Integer>的引用可指向List<Number>的对象,因此说明List<? super Integer>是逆变的。对逆变类型引用可进行数据写入,但是读取的时候,如果不进行强制类型转换,编译是无法通过的。首先List<? super Integer>规定了集合类型的下界为Integer类型,而实际的类型为List<Number>。因此我们在对集合进行数据写入时,写入了Integer类型,而实际存放的是Number类型的数据,根据多态可知此操作可行。但是进行读取的时候,由于读取的实际类型是Number类型,因此,不能将Number类型的数据赋值给Integer引用(子类的引用无法指向父类的对象)。
那么何时使用协变与逆变呢?根据effective Java所写的,当需要写入数据时,使用逆变(? super形式);当需要读取数据时,使用协变(? extends形式);当既要对数据进行读取又要对数据进行写入,使用不变(T)。一句话总结:协变是生产者,逆变是消费者。