Java 泛型的协变与逆变
协变、逆变、抗变
协变,逆变,抗变等概念是从数学中来的,在编程语言Java/Kotlin/C#
中主要应用在泛型上。描述的是两个类型集合之间的继承关系。
第一个集合为:Animal、Dog
, Dog extends Animal
第二个集合为:List<Animal>
、List<Dog>
在Java/Kotlin/C#
中,由于Dog
是Animal
的子类型,那么List<Dog>
也是List<Animal>
的子类型吗?实则不然,两个列表是两个完全不同的类型。协变、逆变、抗变
描述的就是两个类型集合间的关系。
- 协变(Covariance):
List<Dog>
是List<Animal>
的子类型 - 逆变(Contravariance):
List<Animal>
是List<Dog>
的子类型 - 抗变(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]);
将一个子类型数组的引用,协变后成功赋值给了父类型的数组引用。就会引发通过 objArr
对 strArr
赋值了一个其他子类型的元素,比如 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
,使得含有泛型类型形参 与 返回值的方法发生了改变。
- 含有泛型类型返回值的方法:可以正常获取
- 含有泛型类型形参的方法:无法在传入任何非 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));}
}
协变、逆变小结
协变体现在方法的返回值类型为泛型类型时。
逆变体现在方法的参数类型为泛型类型时。
如下:
- 只读取,且类型满足协变关系,使用
? extends T
- 只写入,且类型满足逆变关系,使用
? 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 泛型的协变与逆变相关推荐
- Java泛型的协变与逆变
泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods have same erasure ...
- java泛型的逆变_Java泛型的协变与逆变
泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods have same erasure ...
- 【软件构造】--Java中的协变与逆变
提示:本文主要讨论Java中的协变与逆变 Java中的协变与逆变 前言 一.Liskov替换原则(LSP) 二.协变(Covariance)和逆变(Contravariance) 1.概念 三 讨论 ...
- C# 泛型的协变和逆变
1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...
- Scala泛型:协变和逆变
- 对泛型上下限(协变,逆变)理解的拙见
首先要了解一下以下几点: 子类是对父类的扩展,拥有比父类更多的内容,类似包裹关系. 接收类型的继承关系级别要大于等于该类,也就是说只有其父类和本身能正常接收,否则需要强制转型. 子类转型成父类,会让父 ...
- java协变 生产者理解_Java进阶知识点:协变与逆变
一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...
- Java进阶知识点:协变与逆变
一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...
- 泛型--协变与逆变(转)
对于泛型的知识,一直比较模糊,现在有机会整理一下,突发发现C#还有很多你不知道的东东,继续.NET FrameWork中泛型的协变与逆变: 1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的 ...
最新文章
- 解决naigos+pnp4nagios部分不出图的问题
- 点对点信道互连以太网实验_汽车以太网 – 引领汽车IVN向多速以太网过渡
- could not export python function call python_value. Remove calls to Python functions before export
- 网页快照是什么?对SEO优化有什么作用?
- python开发cs软件_python cs架构实现简单文件传输
- mysql和oracle的通用存储,MySQL与Oracle在使用上的一些区别
- android光传感实现摩斯密码,根据莫尔斯代码 - Android的闪烁闪光。 如何避免ANR次数由于睡觉? (火炬APP)...
- 基于opencv 的OCR小票识别(1)
- java 睡眠1s_Java sleep():线程睡眠
- STM32读写FPGA存储器EPCS器件(EPCS1、EPCS4)
- A component is changing an uncontrolled input to be controlled. This is likely caused by the value
- 花生日记涉传销,给了互联网企业什么启示
- [转载]Numpy 基本除法运算和模运算
- 恶意融资与上市公司的股权结构研究
- 解决java编译错误( 程序包javax.servlet不存在javax.servlet.*)
- nike air max 1 leopard internationaal meest
- android游戏开发原理及关键技术
- 浪涌测试如何进行试验配置
- ucos移植到stm32上的中断小小改进
- DVB-S2中的LDPC