当前位置:网站首页>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) | 值 |
全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!
边栏推荐
- tongweb设置gzip
- 小程序启动性能优化实践
- H.265编码原理入门
- Dry goods sorting! How about the development trend of ERP in the manufacturing industry? It's enough to read this article
- 【对象数组的排序】
- How do enterprises choose the appropriate three-level distribution system?
- OpenGL - Lighting
- SMT32H7系列DMA和DMAMUX的一点理解
- The writing speed is increased by dozens of times, and the application of tdengine in tostar intelligent factory solution
- How to implement complex SQL such as distributed database sub query and join?
猜你喜欢
SMT32H7系列DMA和DMAMUX的一点理解
idea用debug调试出现com.intellij.rt.debugger.agent.CaptureAgent,导致无法进行调试
Oracle combines multiple rows of data into one row of data
基于宽表的数据建模应用
OpenGL - Model Loading
How to choose the right chain management software?
从“化学家”到开发者,从甲骨文到 TDengine,我人生的两次重要抉择
Can't find the activitymainbinding class? The pit I stepped on when I just learned databinding
What should we pay attention to when developing B2C websites?
Using request headers to develop multi terminal applications
随机推荐
Vs code problem: the length of long lines can be configured through "editor.maxtokenizationlinelength"
Principle and performance analysis of lepton lossless compression
Go 语言使用 MySQL 的常见故障分析和应对方法
What should we pay attention to when developing B2C websites?
[listening for an attribute in the array]
Online chain offline integrated chain store e-commerce solution
C language - input array two-dimensional array a from the keyboard, and put 3 in a × 5. The elements in the third column of the matrix are moved to the left to the 0 column, and the element rows in ea
Unity SKFramework框架(二十四)、Avatar Controller 第三人称控制
Kotlin introductory notes (II) a brief introduction to kotlin functions
Develop and implement movie recommendation applet based on wechat cloud
Unity skframework framework (XXII), runtime console runtime debugging tool
From "chemist" to developer, from Oracle to tdengine, two important choices in my life
Community group buying exploded overnight. How should this new model of e-commerce operate?
About getfragmentmanager () and getchildfragmentmanager ()
OpenGL - Coordinate Systems
Community group buying has triggered heated discussion. How does this model work?
The popularity of B2B2C continues to rise. What are the benefits of enterprises doing multi-user mall system?
TDengine 连接器上线 Google Data Studio 应用商店
【ManageEngine】如何利用好OpManager的报表功能
分布式数据库下子查询和 Join 等复杂 SQL 如何实现?