协变、逆变、抗变

协变,逆变,抗变等概念是从数学中来的,在编程语言Java/Kotlin/C#中主要应用在泛型上。描述的是两个类型集合之间的继承关系。

第一个集合为:Animal、Dog , Dog extends Animal
第二个集合为:List<Animal>List<Dog>

Java/Kotlin/C#中,由于DogAnimal的子类型,那么List<Dog>也是List<Animal>的子类型吗?实则不然,两个列表是两个完全不同的类型。协变、逆变、抗变描述的就是两个类型集合间的关系。

  1. 协变(Covariance):List<Dog>List<Animal>的子类型
  2. 逆变(Contravariance): List<Animal>List<Dog>的子类型
  3. 抗变(Invariant): List<Animal>List<Dog>二者间没有任何继承关系

数组的协变

由于历史原因,数组是默认支持协变的。

Object[] objArr = new Object[2];
String[] strArr = new String[] {"A"};
objArr = strArr;
// objArr[0] = 1; // 编译通过,但运行时会抛出:ArrayStoreException
objArr[0] = "B"; // successSystem.out.println(objArr[0]);

将一个子类型数组的引用,协变后成功赋值给了父类型的数组引用。就会引发通过 objArrstrArr 赋值了一个其他子类型的元素,比如 Integer 10。所以 java 语言在运行时会检查并抛出 ArrayStoreException。很明显这种只能在运行时才暴露问题的机制很不友好,这种代码不应该被成功编译,或者就不支持协变这种机制。

java 在泛型编程中的做法是,支持协变逆变这两种特性,并同时在编译期检查数据类型的正确性。当我们在开发工具中,如 idea 编写 Stream 代码时,细心的同学会发现每次调用一个功能函数如 map、collect 后,当前的泛型类型都在动态变化,就犹如编译时类型推导

泛型的抗变性

List<Object> objList = new ArrayList<>();
List<String> strList = new ArrayList<>();// objList = strList; 编译失败

泛型不同数组,默认是具有抗变性的。比如上面的例子,即使 String 是 Object 的子类,但也无法直接扩展为 List<Object>。如果可以这么做,就会发生数组的 ArrayStoreException 的情况:

objList = strList;
objList.add(100);
String str = objList.get(0);

就如开头时提到的:List<String>List<Object> 就是两种不同的类型(虽然 String 与 Object 存在继承关系),二者类型没有任何关系。这样虽然代码上安全了许多,但大大降低了代码的灵活性。如有些场景,为了代码的通用性,仍需要协变、逆变的这种特性打破泛型的抗变性,使得可以处理更多泛型类型的数据。不然处理一些含有泛型的数据时,是很难做到更好的兼容的,只能针对于每种泛型类型都编写一个方法。下面会逐步介绍泛型的协变与逆变。

泛型类型的协变

前面说到在泛型中默认具有抗变性,那么如何打破这种限制?上界限定符: ? extends T

协变的限制

List<Double> dList = new ArrayList<>();
List<? extends Number> nList = new ArrayList<Number>();
List<? extends Number> nList2 = new ArrayList<Double>();
// List<? extends Number> nList2 = new ArrayList<Object>(); // 编译失败
nList = dList;
nList2 = dList;

nList = dList 编译通过!那么是否还会存在类似数组的 ArrayStoreException 的情况吗?我们尝试在 nList 中分别添加 Double、Integer 元素。

nList.add(1.23D); // 编译失败
nList.add(123); // 编译失败

这正是协变的代价:无法在向 list 中添加没有 ? extends 修饰时(协变前)能正常添加的数据。只是泛型的做法更加直接,无论这个元素的类型是否为正确的,都不让添加,避免存储异常。假如可以新增数据,在 nList 添加了一个 Long 元素:

nList = dList;
nList.add(10L);

是不是又发生了存储异常的情况。

但 null 是一种特殊情况

nList.add(null); // 编译成功

协变的好处

可以正常的获取元素,元素类型为协变父类型:Number

Number number = nList.get(0);

协变生效的位置

生效位置:方法形参 & 返回值

为了直观的查看协变的机制,我们不在使用 ArrayList,而是通过一些自定义的类来进行测试。

static class AnimalFactory<T> {public T provide() {return null;}public void receive(T t) {}
}static class Animal {}static class Cat extends Animal {}static class Dog extends Animal {}public static void test() {// 协变前AnimalFactory<Animal> factory = new AnimalFactory<>();Animal provide1 = factory.provide();factory.receive(new Cat());// 协变后AnimalFactory<? extends Animal> factory2 = new AnimalFactory<>();Animal provide2 = factory2.provide(); // 返回值类型为泛型类型// factory2.receive(new Cat()); // 方法形参为泛型类型 (编译失败)factory2.receive(null); // 但可以传递 null
}

factory2 的泛型类型已经从 Animal 协变为了 ? extends Animal,使得含有泛型类型形参 与 返回值的方法发生了改变。

  1. 含有泛型类型返回值的方法:可以正常获取
  2. 含有泛型类型形参的方法:无法在传入任何非 null 的实例

协变的应用

编写一个方法,可以对泛型类型为 Number 的列表进行浮点数求和统计

public static void main(String[] args) {List<Byte> bList = new ArrayList<>();bList.add((byte) 1);bList.add((byte) 2);List<Double> dList = new ArrayList<>();dList.add(1D);dList.add(2D);double v1 = doubleSum(bList);double v2 = doubleSum(dList);System.out.println("v1:" + v1 + "\tv2:" + v2);
}static double sum2Double(List<? extends Number> numbers) {double res = 0;for (Number number : numbers) {res += number.doubleValue();}return res;
}

泛型类型的逆变

逆变与协变是相对的,表示泛型类型可为指定类型自身及其父类。通过下界限定符: ? super T 进行声明。

逆变的好处

List<Object> objList = new ArrayList<>();
List<? super Number> nList = new ArrayList<Number>();
List<? super Number> nList2 = new ArrayList<Object>();
// List<? super Number> nList2 = new ArrayList<Double>(); // 编译失败
nList = objList;

nList = objList 编译通过!那么是否还会存在类似数组的 ArrayStoreException 的情况吗?我们尝试在 nList 中分别添加 Number 、Object 元素。

nList.add(new Number() {@Overridepublic int intValue() {return 0;}@Overridepublic long longValue() {return 0;}@Overridepublic float floatValue() {return 0;}@Overridepublic double doubleValue() {return 0;}
});
nList.add(1.23D);
// nList.add(new Object()); // 编译失败

启用逆变后,可以正常的在列表中添加元素,但可添加的元素类型为泛型类型自身及其子类型
之所以能够添加泛型类型的子类类型元素,是因为下界限定符限定列表中的元素类型为泛型类型的父类类型,而泛型类型的子类也一定是泛型类型父类的子类

逆变的限制

无法在正常获取元素,因为不知道元素类型究竟是泛型类型的哪个父类型。这正是逆变的代价:无法在获取 list 中添加没有 ? super 修饰时(协变前)能正常获取的数据

Number n = nList.get(0); // 编译失败

但 Object 是一种特殊情况,因为它是一切对象的父类。

Object obj = nList.get(0);

逆变生效的位置

生效位置:方法形参 & 返回值

static class AnimalFactory<T> {public T provide() {return null;}public void receive(T t) {}
}static class Animal {}static class Cat extends Animal {}static class Dog extends Animal {}public static void test() {// 逆变前AnimalFactory<Animal> factory1 = new AnimalFactory<>();Animal provide1 = factory1.provide();factory1.receive(new Cat());// 逆变后AnimalFactory<? super Animal> factory2 = new AnimalFactory<>();// Animal provide2 = factory2.provide(); // 返回值类型为泛型类型 (编译失败)// factory2.receive(new Object()); // 方法形参为泛型类型的父类 (编译失败)factory2.receive(new Animal()); // 方法形参为泛型类型自身factory2.receive(new Cat()); // 方法形参为泛型类型的子类
}

逆变的应用

编写一个方法,可以对泛型类型为 Number 的列表进行数据过滤

static <T> Collection<T> remove(Collection<T> col, Predicate<? super T> filter) {List<T> removeList = new ArrayList<>();for (T t : col) {if (filter.test(t)) {removeList.add(t);}}col.removeAll(removeList);return col;
}

协变与逆变的结合应用

public static void main(String[] args) {List<Integer> list1 = Lists.newArrayList(1, 2, 3);List<Integer> list2 = Lists.newArrayList(4, 5, 6);copy(list1, list2);System.out.println(list1); // [1, 2, 3]System.out.println(list2); // [1, 2, 3, 4, 5, 6]
}public static <T> void copy(List<? extends T> src, List<? super T> dest) {int size = src.size();for (int i = 0; i < size; i++) {dest.add(i, src.get(i));}
}

协变、逆变小结

协变体现在方法的返回值类型为泛型类型时。
逆变体现在方法的参数类型为泛型类型时。
如下:

  1. 只读取,且类型满足协变关系,使用 ? extends T
  2. 只写入,且类型满足逆变关系,使用 ? super T

任意通配符

通配符 ?,表示任意类型。仅有协变、逆变的两种特殊情况。

public static void any(List<?> list) {Object o = list.get(0);list.add(0, null);list.add(0, new Object()); // 编译失败
}

extends 通配符的其他应用

应用一:另一种方法形参类型限定

static class Animal {}static class Cat extends Animal {}static class Dog extends Animal {}// 限定泛型类型为 Animal 与其子类型,返回值类型只能为 Animal。语义同 createAnimal2
public static Animal createAnimal1(Class<? extends Animal> animalClass) {try {return animalClass.newInstance();} catch (Exception e) {throw new RuntimeException(e);}
}// 结合方法一、二的特性
// 限定泛型类型为 Animal 与其子类型,返回值类型只能为 Animal。语义同 createAnimal1
public static <T extends Animal> Animal createAnimal2(Class<T> animalClass) {try {return animalClass.newInstance();} catch (Exception e) {throw new RuntimeException(e);}
}// 限定泛型类型为 Animal 与其子类型,返回值类型可以为具体的传入的 T 类型,而不是只能返回 Animal 类型
public static <T extends Animal> T createAnimal3(Class<? extends T> animalClass) {try {return animalClass.newInstance();} catch (Exception e) {throw new RuntimeException(e);}
}public static void extendsTest1() {Animal cat1 = createAnimal1(Cat.class);Animal dog1 = createAnimal1(Dog.class);Animal animal1 = createAnimal1(Animal.class);Animal cat2 = createAnimal2(Cat.class);Animal dog2 = createAnimal2(Dog.class);Animal animal2 = createAnimal2(Animal.class);Cat cat3 = createAnimal3(Cat.class);Dog dog3 = createAnimal3(Dog.class);Animal animal3 = createAnimal3(Animal.class);
}

应用二:应用在类、接口泛型

abstract static class Person {public void born() { System.out.println("嘤嘤嘤"); }
}static class Man extends Person {}static class Woman extends Person {}static class PersonFactory<T extends Person> {public T create(Class<T> personClass) {try {T t = personClass.newInstance();t.born();return t;} catch (Exception e) {throw new RuntimeException(e);}}
}public static void extendsTest2() {PersonFactory<Man> manFactory = new PersonFactory<>();Man man = manFactory.create(Man.class);// manFactory.create(Woman.class); // 编译失败PersonFactory<Woman> womanFactory = new PersonFactory<>();Woman woman = womanFactory.create(Woman.class);// womanFactory.create(Man.class); // 编译失败
}

Java 泛型的协变与逆变相关推荐

  1. Java泛型的协变与逆变

    泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods  have same erasure ...

  2. java泛型的逆变_Java泛型的协变与逆变

    泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods  have same erasure ...

  3. 【软件构造】--Java中的协变与逆变

    提示:本文主要讨论Java中的协变与逆变 Java中的协变与逆变 前言 一.Liskov替换原则(LSP) 二.协变(Covariance)和逆变(Contravariance) 1.概念 三 讨论 ...

  4. C# 泛型的协变和逆变

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...

  5. Scala泛型:协变和逆变

  6. 对泛型上下限(协变,逆变)理解的拙见

    首先要了解一下以下几点: 子类是对父类的扩展,拥有比父类更多的内容,类似包裹关系. 接收类型的继承关系级别要大于等于该类,也就是说只有其父类和本身能正常接收,否则需要强制转型. 子类转型成父类,会让父 ...

  7. java协变 生产者理解_Java进阶知识点:协变与逆变

    一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...

  8. Java进阶知识点:协变与逆变

    一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...

  9. 泛型--协变与逆变(转)

    对于泛型的知识,一直比较模糊,现在有机会整理一下,突发发现C#还有很多你不知道的东东,继续.NET FrameWork中泛型的协变与逆变: 1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的 ...

最新文章

  1. 解决naigos+pnp4nagios部分不出图的问题
  2. 点对点信道互连以太网实验_汽车以太网 – 引领汽车IVN向多速以太网过渡
  3. could not export python function call python_value. Remove calls to Python functions before export
  4. 网页快照是什么?对SEO优化有什么作用?
  5. python开发cs软件_python cs架构实现简单文件传输
  6. mysql和oracle的通用存储,MySQL与Oracle在使用上的一些区别
  7. android光传感实现摩斯密码,根据莫尔斯代码 - Android的闪烁闪光。 如何避免ANR次数由于睡觉? (火炬APP)...
  8. 基于opencv 的OCR小票识别(1)
  9. java 睡眠1s_Java sleep():线程睡眠
  10. STM32读写FPGA存储器EPCS器件(EPCS1、EPCS4)
  11. A component is changing an uncontrolled input to be controlled. This is likely caused by the value
  12. 花生日记涉传销,给了互联网企业什么启示
  13. [转载]Numpy 基本除法运算和模运算
  14. 恶意融资与上市公司的股权结构研究
  15. 解决java编译错误( 程序包javax.servlet不存在javax.servlet.*)
  16. nike air max 1 leopard internationaal meest
  17. android游戏开发原理及关键技术
  18. 浪涌测试如何进行试验配置
  19. ucos移植到stm32上的中断小小改进
  20. DVB-S2中的LDPC

热门文章

  1. 暑假实习——微信小商城
  2. DFS和BFS的区别
  3. x86汇编_CMP指令_笔记_28
  4. 使用iphone作为zoom会议的摄像头
  5. HDU 3091(动态规划-状态压缩)
  6. uni-app的分包过程
  7. NILM论文笔记:R.Reddy, et al: A feature fusion technique for improved NILM
  8. 微信群裂变微云群活码系统上线
  9. Fibonacci数列取余10007
  10. 写日记(周记)的重要性