当前位置:网站首页>90%的人都不懂的泛型,泛型的缺陷和应用场景
90%的人都不懂的泛型,泛型的缺陷和应用场景
2022-07-05 09:30:00 【hi-dhl】
作者简介: hi 大家好,我是 dhl,正在维护自己的 个人网站 ,专注分享最新技术原创文章,涉及 Kotlin、Jetpack、算法动画、数据结构、系统源码 等等。
转载说明:未获得授权,禁止转载。
全文分为 视频版 和 文字版,
- 文字版: 文字侧重细节和深度,有些知识点,视频不好表达,文字描述的更加准确
- 视频版: 视频会更加的直观,看完文字版,在看视频,知识点会更加清楚
视频版 bilibili 地址:https://b23.tv/AdLtUGf
泛型对于每个开发者而言并不陌生,平时在项目中会经常见到,但是有很多小伙伴们,每次见到通配符 ? extends 、 ? super 、 out 、 in 都傻傻分不清楚它们的区别,以及在什么情况下使用。
通过这篇文章将会学习的到以下内容。
- 为什么要有泛型
- Kotlin 和 Java 的协变
- Kotlin 和 Java 的逆变
- 通配符
? extends、? super、out、in的区别和应用场景 - Kotlin 和 Java 数组协变的不同之处
- 数组协变的缺陷
- 协变和逆变的应用场景
为什么要有泛型
在 Java 和 Kotlin 中我们常用集合( List 、 Set 、 Map 等等)来存储数据,而在集合中可能存储各种类型的数据,现在我们有四种数据类型 Int 、 Float 、 Double 、 Number,假设没有泛型,我们需要创建四个集合类来存储对应的数据。
class IntList{ ...... }
class FloatList{ ...... }
class DoubleList{ ...... }
class NumberList{ ...... }
......
更多
如果有更多的类型,就需要创建更多的集合类来保存对应的数据,这显示是不可能的,而泛型是一个 “万能的类型匹配器”,同时有能让编译器保证类型安全。
泛型将具体的类型( Int 、 Float 、 Double 等等)声明的时候使用符号来代替,使用的时候,才指定具体的类型。
// 声明的时候使用符号来代替
class List<E>{
}
// 在 Kotlin 中使用,指定具体的类型
val data1: List<Int> = List()
val data2: List<Float> = List()
// 在 Java 中使用,指定具体的类型
List<Integer> data1 = new List();
List<Float> data2 = new List();
泛型很好的帮我们解决了上面的问题,但是随之而来出现了新的问题,我们都知道 Int 、 Float 、 Double 是 Number 子类型, 因此下面的代码是可以正常运行的。
// Kotlin
val number: Number = 1
// Java
Number number = 1;
我们花三秒钟思考一下,下面的代码是否可以正常编译。
List<Number> numbers = new ArrayList<Integer>();
答案是不可以,正如下图所示,编译会出错。

这也就说明了泛型是不可变的,IDE 认为 ArrayList<Integer> 不是 List<Number> 子类型,不允许这么赋值,那么如何解决这个问题呢,这就需要用到协变了,协变允许上面的赋值是合法的。
Kotlin 和 Java 的协变
- 在 Java 中用通配符
? extends T表示协变,extends限制了父类型T,其中?表示未知类型,比如? extends Number,只要声明时传入的类型是Number或者Number的子类型都可以 - 在 Kotlin 中关键字
out T表示协变,含义和 Java 一样
现在我们将上面的代码修改一下,在花三秒钟思考一下,下面的代码是否可以正常编译。
// kotlin
val numbers: MutableList<out Number> = ArrayList<Int>()
// Java
List<? extends Number> numbers = new ArrayList<Integer>();
答案是可以正常编译,协变通配符 ? extends Number 或者 out Number 表示接受 Number 或者 Number 子类型为对象的集合,协变放宽了对数据类型的约束,但是放宽是有代价的,我们在花三秒钟思考一下,下面的代码是否可以正常编译。
// Koltin
val numbers: MutableList<out Number> = ArrayList<Int>()
numbers.add(1)
// Java
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(1)
调用 add() 方法会编译失败,虽然协变放宽了对数据类型的约束,可以接受 Number 或者 Number 子类型为对象的集合,但是代价是 无法添加元素,只能获取元素,因此协变只能作为生产者,向外提供数据。
为什么无法添加元素
因为 ? 表示未知类型,所以编译器也不知道会往集合中添加什么类型的数据,因此索性不允许往集合中添加元素。
但是如果想让上面的代码编译通过,想往集合中添加元素,这就需要用到逆变了。
Kotlin 和 Java 的逆变
逆变其实是把继承关系颠倒过来,比如 Integer 是 Number 的子类型,但是 Integer 加逆变通配符之后,Number 是 ? super Integer 的子类,如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8bRLR2Qd-1655947414076)(https://img.hi-dhl.com/16551339994410.jpg)]
- 在 Java 中用通配符
? super T表示逆变,其中?表示未知类型,super主要用来限制未知类型的子类型T,比如? super Number,只要声明时传入是Number或者Number的父类型都可以 - 在 Kotlin 中关键字
in T表示逆变,含义和 Java 一样
现在我们将上面的代码简单修改一下,在花三秒钟思考一下是否可以正常编译。
// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
答案可以正常编译,逆变通配符 ? super Number 或者关键字 in 将继承关系颠倒过来,主要用来限制未知类型的子类型,在上面的例子中,编译器知道子类型是 Number,因此只要是 Number 的子类都可以添加。
逆变可以往集合中添加元素,那么可以获取元素吗?我们花三秒钟时间思考一下,下面的代码是否可以正常编译。
// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
numbers.get(0)
// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
numbers.get(0);
无论调用 add() 方法还是调用 get() 方法,都可以正常编译通过,现在将上面的代码修改一下,思考一下是否可以正常编译通过。
// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
val item: Int = numbers.get(0)
// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
int item = numbers.get(0);
调用 get() 方法会编译失败,因为 numbers.get(0) 获取的的值是 Object 的类型,因此它不能直接赋值给 int 类型,逆变和协变一样,放宽了对数据类型的约束,但是代价是 不能按照泛型类型读取元素,也就是说往集合中添加 int 类型的数据,调用 get() 方法获取到的不是 int 类型的数据。
对这一小节内容,我们简单的总结一下。
| 关键字(Java/Kotlin) | 添加 | 读取 | |
|---|---|---|---|
| 协变 | ? extends / out | ||
| 逆变 | ? super / in |
Kotlin 和 Java 数组协变的不同之处
无论是 Kotlin 还是 Java 它们协变和逆变的含义的都是一样的,只不过通配符不一样,但是他们也有不同之处。
Java 是支持数组协变,代码如下所示:
Number[] numbers = new Integer[10];
但是 Java 中的数组协变有缺陷,将上面的代码修改一下,如下所示。
Number[] numbers = new Integer[10];
numbers[0] = 1.0;
可以正常编译,但是运行的时候会崩溃。

因为最开始我将 Number[] 协变成 Integer[],接着往数组里添加了 Double 类型的数据,所以运行会崩溃。
而 Kotlin 的解决方案非常的干脆,不支持数组协变,编译的时候就会出错,对于数组逆变 Koltin 和 Java 都不支持。
协变和逆变的应用场景
协变和逆变应用的时候需要遵循 PECS(Producer-Extends, Consumer-Super)原则,即 ? extends 或者 out 作为生产者,? super 或者 in 作为消费者。遵循这个原则的好处是,可以在编译阶段保证代码安全,减少未知错误的发生。

协变应用
- 在 Java 中用通配符
? extends表示协变 - 在 Kotlin 中关键字
out表示协变
协变只能读取数据,不能添加数据,所以只能作为生产者,向外提供数据,因此只能用来输出,不用用来输入。
在 Koltin 中一个协变类,参数前面加上 out 修饰后,这个参数在当前类中 只能作为函数的返回值,或者修饰只读属性 ,代码如下所示。
// 正常编译
interface ProduceExtends<out T> {
val num: T // 用于只读属性
fun getItem(): T // 用于函数的返回值
}
// 编译失败
interface ProduceExtends<out T> {
var num : T // 用于可变属性
fun addItem(t: T) // 用于函数的参数
}
当我们确定某个对象只作为生产者时,向外提供数据,或者作为方法的返回值时,我们可以使用 ? extends 或者 out。
- 以 Kotlin 为例,例如
Iterator#next()方法,使用了关键字out,返回集合中每一个元素

- 以 Java 为例,例如
ArrayList#addAll()方法,使用了通配符? extends

传入参数 Collection<? extends E> c 作为生产者给 ArrayList 提供数据。
逆变应用
- 在 Java 中使用通配符
? super表示逆变 - 在 Kotlin 中使用关键字
in表示逆变
逆变只能添加数据,不能按照泛型读取数据,所以只能作为消费者,因此只能用来输入,不能用来输出。
在 Koltin 中一个逆变类,参数前面加上 in 修饰后,这个参数在当前类中 只能作为函数的参数,或者修饰可变属性 。
// 正常编译,用于函数的参数
interface ConsumerSupper<in T> {
fun addItem(t: T)
}
// 编译失败,用于函数的返回值
interface ConsumerSupper<in T> {
fun getItem(): T
}
当我们确定某个对象只作为消费者,当做参数传入时,只用来添加数据,我们使用通配符 ? super 或者关键字 in,
- 以 Kotlin 为例,例如扩展方法
Iterable#filterTo(),使用了关键字in,在内部只用来添加数据

- 以 Java 为例,例如
ArrayList#forEach()方法,使用了通配符? super

不知道小伙伴们有没有注意到,在上面的源码中,分别使用了不同的泛型标记符 T 和 E,其实我们稍微注意一下,在源码中有几个高频的泛型标记符 T 、 E 、 K 、 V 等等,它们分别应用在不同的场景。
| 标记符 | 应用场景 |
|---|---|
| T(Type) | 类 |
| E(Element) | 集合 |
| K(Key) | 键 |
| V(Value) | 值 |
全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!
边栏推荐
- VS Code问题:长行的长度可通过 “editor.maxTokenizationLineLength“ 进行配置
- From "chemist" to developer, from Oracle to tdengine, two important choices in my life
- Kotlin introductory notes (IV) circular statements (simple explanation of while, for)
- LeetCode 503. Next bigger Element II
- 百度APP 基于Pipeline as Code的持续集成实践
- Go 语言使用 MySQL 的常见故障分析和应对方法
- 【el-table如何禁用】
- Kotlin introductory notes (VII) data class and singleton class
- 一篇文章带你走进cookie,session,Token的世界
- uni-app---uni.navigateTo跳转传参使用
猜你喜欢

LeetCode 496. Next larger element I

VS Code问题:长行的长度可通过 “editor.maxTokenizationLineLength“ 进行配置

代码语言的魅力
![[ManageEngine] how to make good use of the report function of OpManager](/img/15/dc15e638ae86d6cf1d5b989fe56611.jpg)
[ManageEngine] how to make good use of the report function of OpManager

Solve liquibase – waiting for changelog lock Cause database deadlock
![[reading notes] Figure comparative learning gnn+cl](/img/44/2e13d63ef654663852cbccb342b838.png)
[reading notes] Figure comparative learning gnn+cl

Principle and performance analysis of lepton lossless compression

What should we pay attention to when entering the community e-commerce business?

初识结构体

First understanding of structure
随机推荐
H.265编码原理入门
正式上架!TDengine 插件入驻 Grafana 官网
一文详解图对比学习(GNN+CL)的一般流程和最新研究趋势
【对象数组a与对象数组b取出id不同元素赋值给新的数组】
百度交易中台之钱包系统架构浅析
LeetCode 503. 下一个更大元素 II
TDengine可通过数据同步工具 DataX读写
Kotlin introductory notes (VIII) collection and traversal
What should we pay attention to when entering the community e-commerce business?
Talking about the difference between unittest and pytest
一篇文章带你走进cookie,session,Token的世界
Idea debugs com intellij. rt.debugger. agent. Captureagent, which makes debugging impossible
一次 Keepalived 高可用的事故,让我重学了一遍它
SQL learning group by multi table grouping scenario
mysql安装配置以及创建数据库和表
代码语言的魅力
LeetCode 496. 下一个更大元素 I
Tdengine offline upgrade process
Kotlin introductory notes (VII) data class and singleton class
Tongweb set gzip