美文网首页
java的协变与逆变

java的协变与逆变

作者: hello_kd | 来源:发表于2018-11-24 16:17 被阅读147次

在日常的开发中,你是否经常看见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)。一句话总结:协变是生产者,逆变是消费者。

相关文章

  • JAVA泛型与类型安全

    1. 基础泛型 2. 协变与逆变与不变 协变 简单来说即: Java中的数组是协变的 逆变与协变相对,逆转了类型关...

  • java 不变、协变、逆变

    java 不变、协变、逆变 前言 先说结论,java 的 List 是不变的,java 的 array 是协变的。...

  • Kotlin 泛型协变与逆变的理解

    协变与逆变定义 逆变与协变用来描述类型转换后的继承关系 协变:如果 A 是 B 的子类型,并且Generic 也...

  • Java中的桥接方法与泛型的逆变和协变

    泛型的协变和逆变是什么?对应于Java当中,协变对应的就是,而逆变对应的就是

  • 协变和逆变

    Java的泛型只有通配符?和extends、super,没有语法上的协变和逆变。 什么是协变和逆变? 在混合OO和...

  • Java 泛型与通配符

    参考地址:《Java 泛型,你了解类型擦除吗?》 《Java中的逆变与协变》 《java 泛型中 T、E .....

  • Java协变和逆变

    泛型的协变与逆变 协变与逆变用来描述类型转换(type transformation)后的继承关系,其定义如下:如...

  • Java逆变与协变

    引子 《Effective Java》中第25条中《列表优于数组》中提到数组是协变的,相反泛型是不可变的 其实用于...

  • Java中的逆变与协变

    Java中的逆变与协变 原文:http://www.cnblogs.com/en-heng/p/5041124.h...

  • java的协变与逆变

    在日常的开发中,你是否经常看见List、List、 List 、List

网友评论

      本文标题:java的协变与逆变

      本文链接:https://www.haomeiwen.com/subject/ilpeqqtx.html