当前位置:网站首页>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) | 值 |
全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!
边栏推荐
- TDengine 离线升级流程
- What are the advantages of the live teaching system to improve learning quickly?
- What should we pay attention to when entering the community e-commerce business?
- Go 语言使用 MySQL 的常见故障分析和应对方法
- Tdengine can read and write through dataX, a data synchronization tool
- Three-level distribution is becoming more and more popular. How should businesses choose the appropriate three-level distribution system?
- Using request headers to develop multi terminal applications
- Why do offline stores need cashier software?
- SQL learning group by multi table grouping scenario
- [reading notes] Figure comparative learning gnn+cl
猜你喜欢
Solve the problem of no all pattern found during Navicat activation and registration
mysql安装配置以及创建数据库和表
如何正确的评测视频画质
How to implement complex SQL such as distributed database sub query and join?
C语言-从键盘输入数组二维数组a,将a中3×5矩阵中第3列的元素左移到第0列,第3列以后的每列元素行依次左移,原来左边的各列依次绕到右边
图神经网络+对比学习,下一步去哪?
Lepton 无损压缩原理及性能分析
Solve liquibase – waiting for changelog lock Cause database deadlock
LeetCode 496. 下一个更大元素 I
Unity SKFramework框架(二十三)、MiniMap 小地图工具
随机推荐
Android privacy sandbox developer preview 3: privacy, security and personalized experience
[two objects merged into one object]
OpenGL - Coordinate Systems
[sourcetree configure SSH and use]
【对象数组a与对象数组b取出id不同元素赋值给新的数组】
【sourceTree配置SSH及使用】
Analysis of eventbus source code
Resolve the horizontal (vertical) sliding conflict between viewpager and WebView
Kotlin introductory notes (VIII) collection and traversal
VS Code问题:长行的长度可通过 “editor.maxTokenizationLineLength“ 进行配置
LeetCode 496. 下一个更大元素 I
STM32 simple multi-level menu (array table lookup method)
揭秘百度智能测试在测试自动执行领域实践
MySQL installation configuration and creation of databases and tables
LeetCode 503. 下一个更大元素 II
Unity skframework framework (XXII), runtime console runtime debugging tool
Can't find the activitymainbinding class? The pit I stepped on when I just learned databinding
Nips2021 | new SOTA for node classification beyond graphcl, gnn+ comparative learning
Three-level distribution is becoming more and more popular. How should businesses choose the appropriate three-level distribution system?
Kotlin introductory notes (III) kotlin program logic control (if, when)