theme: Chinese-red
kotlin
的泛型基础和java
很像, 所以我建议学习kotlin
的泛型前, 先去学习下java
的泛型, 至少搞懂通配符,<? extends X>
和<? super X>
是怎么回事, 怎么写 泛型函数, 泛型类, 知道泛型的本质是什么?
泛型
泛型: 将类型当作参数传递到类内
参数 | 泛型 |
---|---|
fun funName(参数) | class ClassName<类型> |
传递给函数 | 传递给对象 |
我们需要将类型当作参数传递给对象, 传递的类型可能会被用于定义属性或者用于函数的泛型参数
需要注意:
参数有可变参数
vararg
泛型也是, 可以传递泛型的子类类型
简单示例: 函数, 参数, 属性和类的泛型
fun <T> print(t: T) {
println(t)
}
class GenericsDemo01<T>(val f: T) {
fun print(t: T) {
println(t)
}
}
泛型约束(T : Integer
)
主要内容
- 缩小类型的范围
- 一个约束
T : Integer
==>T extends Integer
- 多个约束
T where T: XXX, T: YYY
==>T extends CharSequence & Appendable
很多时候我们需要将泛型的类型约束在某个界限, 比如: sum函数的泛型
fun <T> sum(a: T, b: T): T {
return a + b // error
}
参数 a
和 参数 b
并不是什么类型都支持 +
这项操作, 所以我们需要对传入的类型参数(泛型)做限制, 像下面这样
fun <T : Integer> sum(a: T, b: T): T {
return a + b
}
这样操作类似于 java 的 <T extends Integer>
, 限定 T
必须继承 Integer
(或者说T
必须是Integer
的子类).
java 的
<T extends Integer>
用于集合的泛型, 而泛型约束通常用于非集合的泛型, 因为集合泛型已经有 协变和逆变 的约束了, 不需要这一章的泛型约束
对的, 这样做就不会出现传入俩 Any 类型
的 a
和 b
做加法运算符这样尴尬的事情
泛型约束不会像集合泛型约束那样严格控制
T
必须是同一个, 你可以这样使用:
private fun <T : Number> printT(a: T, b: T) {
// a = 9999, b = 100.5
println("a = $a, b = $b")
// aClass = class java.lang.Integer, bClass = class java.lang.Double
println("aClass = ${a.javaClass}, bClass = ${b.javaClass}")
}
fun main() {
printT(9999, 100.5)
}
a: T, b: T
中的 T
是两个不一样的类型, 一个是 Integer
, 另一个是 Double
上面那段代码类似于 java 的这段代码
static <T extends Number> void printT(T a, T b) {
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println(a.getClass());
System.out.println(b.getClass());
}
public static void main(String[] args) throws Exception {
printT(10, 20.1);
}
课外: 突然发现 不知道 泛型如何 写 sum 了, 所以想了下, 好像只能使用反射来实现
fun <T> sum(a: T, b: T): T {
val clazz = a.javaClass
val sum = clazz.declaredMethods.firstOrNull { it.name == "sum" } ?: throw Exception("can't find function. T not is a subclass of Number")
return sum.invoke(a, a, b) as T
}
小笔记: 在反射获取
sum
函数时, 发现它的函数签名是:int sum(int, int)
但是我们a: T
类型的T
会被认为是Integer
(泛型只能是Integer
), 而获取sum
却需要int
比较麻烦, 最后发现Integer.TYPE
是拆包类型int
可以考虑从这里下手
为一个泛型添加多个约束
where
类似于 sql
语句的 where
一样
fun <T> ensureTrailingPeriod(seq: T): T where T : CharSequence, T : Appendable {
if (!seq.endsWith(".")) {
seq.append(".")
}
return seq
}
约束的好处不仅仅是让我们知道我们需要的类必须是约束和约束的子类. 同时还会让我们的
T
多出很多约束类的函数(包括扩展函数等)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q2o3R16o-1656299835932)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57a2deccb6904706b201c74d7503a5c2~tplv-k3u1fbpfcp-watermark.image?)]
endsWith
函数是CharSequence
的扩展函数,append
是Appendable
接口的函数
前面的 sum
函数也是
java中也允许多约束泛型
static <T extends CharSequence & Appendable> T ensureTrailingPeriod(T seq)
泛型类型可以为 null 也可以为 non-null
泛型的类型T
类似于平台类型, 是否为空由程序员决定, kotlin 不再进行可空管理, 程序员认为他是 可空类型 它就是可空类型, 程序员认为它非空类型, 它就是非空类型
fun <T> print(t: T) {
t?.let { println(it) }
}
泛型运行时的类型擦除和实化类型
主要内容:
- 类型擦除 和 java 一样(妥妥的糟粕, 给整过来了)
- 实化类型: 用于
is T
和T::class.java
fun <T> isIntList(list: List<T>) {
if (list is List<Int>) { // 这里报错
println("这是错误的")
}
}
fun main() {
val list = listOf(1, 2, 3)
isIntList(list)
}
但是可以这样:
if (list is List<*>)
可以看出, 泛型被类型擦除为 List<Any?>
fun <T> isIntList(list: List<T>) {
if (list is List<Any?>) { // 这样不会报错
println("不会报错了")
}
}
kotlin 编译器可以判断在同一个作用域内的泛型类型
val list = listOf(1, 2, 3)
if (list is List<Int>) {
println("这样是可以的")
}
下面这种情况也会出现问题
实化类型参数 reified T
在运行期间, 类型被当作 Any?
类型, 但它想被强制转换成 T
类型明显是不行的, 这种情况下, 可以考虑使用 inline
和 reified
配合实现
inline fun <reified T> isIntList(list: List<T>) {
if (list is List<T>) { // 这里不会报错
println("不会报错")
}
}
同样的我们调用 is T
也不会报错了
inline fun <reified T> isA(value: Any) = value is T
fun main() {
val a: Int = 10
println(isA<Int>(a))
}
前面我们学过, inline
内联的话, 会把代码拷贝到所有调用的地方, 使用上面这种方式kotlin编译器在运行期间
可以识别到泛型的类型
inline
在之前的章节中是为了提高性能, 消除lambda
参数带来的副作用对象而使用的, 在本章节是为了实化类型, 这是第二个inline
的使用场景
实例化参数的另一种使用场景是, 将 类型做参数传递后, 借助该类型获取 Class
类对象
inline fun <reified T> loadService(): ServiceLoader<T>? {
return ServiceLoader.load(T::class.java)
}
fun main() {
// 以前需要在参数上多一个传递 Class 的参数, 现在不需要了
// val loadService = loadService(Int::class)
val loadService = loadService<Int>()
}
以前需要在参数上多一个传递 Class 的参数, 现在不需要了
变型: 泛型和子类型关系
类、类型和子类型
类和类型的区别
在很多情况下, 类都可以大体上当作类型, 但实际上, 类和类型不是一个东西就比如: 空类型和非空类型, Int
和 Int?
, 请确认下 Int?
是类么?? 不是 那Int?
是类型么? 明显,是类型
又或者: List
是个类而 List<T>
它又是个类型, 且他的类型有很多, 比如: List<Int>
List<Double>
List<Long>
等, 这些都是类型, 而List
是类
子类型关系
子类型说的是一种 父子关系 , 这种关系在 java 的类, java 的数组里存在, 而在 java 的泛型里却不见了
(书本上的内容, 看不懂看下面)任何时候如果需要的是类型 A 的值,你都能够使用类型 B 的值当作 A 使用 , 类型 B 就称为类型 A 的子类型。
说简单点, A类指针(java叫引用)指向B类对象, 那么就可以说 A 的子类型是 B, 就这么简单(
val a: A = B()
)
比如: 现在有个引用 val a: Number
和一个Int
类型的对象10
, 如果引用能够直接指向对象val a: Number = 10
则可以说 Int
是 Number
的子类型, 而同时我们可以说 Number
是 Int
的超类型
简单点: 子类的超类型是父类, 父类的子类型是子类, 只要记住这种关系就好
协变和逆变
高端的概念总会有落地的实现, 我们学习要达到的程度是用最简单的一句话描述这些概念
在生活中, 越宽的桶能够盛放越多的水, 越小的桶能够盛放越少的水, 而我们的类型也是, Any? 是 kotlin 中最宽泛的水桶, 它既能够存放非空的所有对象, 也能够存放可空的所有kotlin对象, 这就是多态的根本, 也是协变和逆变的根本
协变(covariant)
1. 是什么?
协变: 是一种关系, 一种父类引用 指向 子类对象 的关系
-
Number
引用总能够指向Int
对象, 那么Number
的子类型是Int
, 则Number
和Int
是协变的 -
那么同样的
Number[]
引用 总能够 指向Int[]
, 则Number[]
和Int[]
是协变的 -
同样的,
List<Number>
的引用总能够 指向List<Int>
那么我们也能够说:List<Number>
和List<Int>
有协变关系(但在java中失效了)
但, java 因为历史关系, 使用了类型擦除技术, 所以 任何类型变到泛型的话, 就不会有所谓的协变(逆变)关系, 因为到了运行时期 java 总把类型变成 List<Object>
或者 直接是 List
类型, 如果强制开出协变关系, 则会出现一些安全问题
java泛型类型擦除带来的问题
会出现 哥哥 泛型的水桶, 被jvm拿走忘了只能装哥哥了, 装了个 弟弟 泛型类型的对象, 这明显不对, 我的水桶要的只能是 哥哥 或者 哥哥的子类, 最最重要的是 jvm 记性还不好(类型擦除), 会把所有装XXX的水桶, 记成水桶里什么东西都能装
泛型在存在协变关系的数组中, 可以正确的判断出错误:
Integer[] a = new Integer[2];
a[0] = 1000;
Object[] o = a;
o[1] = 'a'; // 这里会报错 java.lang.ArrayStoreException: java.lang.Character
Integer 引用想指向没有子类型关系的 Character对象, 直接报错
如果把上面的数组完全换成集合就会变成如下代码:
List<Integer> list = new ArrayList();
list.add(1000);
List<Object> objList = list; // 父类引用指向子类对象, 按理来说 没错 object --> Integer(但实际上这里不会编译通过的)
objList.add(10.9); // 这里在运行期间将会编译通过, 运行通过, 因为还是 父类引用指向子类的对象, object --> double
对比下有协变的数组:
这种泛型和数组的不一致就表示泛型不存在协变关系
为了解决上面的问题, java 引入了属于 java 的泛型的协变
java泛型对于"消失的协变关系"的解决方案
协变关系, 又有人叫
子类型关系
java 引入了 通配符?
, 然后用 List<? extends Number>
表示协变, 相当于没有类型擦除的 List<Number>
, 接受Number
及Number
的子类存入List<Number>
集合中
所以 List<? extends Number>
集合可以存入
上面这些类的对象
那么他是如何解决的上面那个问题的呢?
答: java 的解决方法很简单, 一刀切, 如果类型是 <? extends Number>
协变的, 那么他就不允许写入, 修改等操作. 只允许读取
我特么, 解决不了问题, 就解决提出问题的人是吧???
小总结:
List<? extends Number>
不好记里面可以存放什么类, 可以直接认为是 支持协变的List<Number>
理解就好了, 支持协变的话,Number
集合可以存入它和它的子类
当然我们也可以认为
?
就是我们写的类,class ? extends Number {}
表示写了个Number
的子类, 意味着?
是子类, 所以?
表示所有的子类
对应于 kotlin
的协变关系
在 kotlin
中, 协变将会是:
1. 在类处类型参数协变
2. 在函数处集合泛型的协变
类处类型参数的协变
interface Producer<out T> {
fun produce() : T
}
out
放在那里的位置, 主要有两个功能:
- 子类型将会被保留(
Producer<Cat>
是Producer<Animal>
的子类) T
只能用在out
位置
in
的位置在函数参数,out
位置在函数返回值, 既是in
又是out
则不需要标记, 同样的out
标记的泛型只能读取, 不能写入,in
标记的泛型只能写入不能读取(和java
优点不太一样???)
上面的
transform
函数, 参数明显是范围越大越好, 所以使用? super Number
也就是kotlin
中的in T
, 而通过函数transform
函数处理之后返回的范围应该越小越好, 所以使用? extends Number
也就是out T
MutableList
不能使用out
, 因为out
只能往外输出(读取)对象而不能往里写入对象, 但MutableList
可以写入可以读取, 明显矛盾- 协变后的集合不允许写入, 只允许读取
- 协变的
out B
表示只能填入B
或者B的子类
函数处集合泛型的协变
和 java 类似的用法
out T
对应了 java 的 ? extends T
in T
对应了 java 的 ? super T
我们使用下面的代码来了解协变
的一些特性
open class A
open class B : A()
open class C : B()
open class D : C()
class E
-
首先协变
out B
可以看作是? extends B
也就是所谓的上界
, 说白了只能接受B
以及B
的子类val l0: ArrayList<out B> = arrayListOf(B(), C(), D()) // 但如果我们加添 B 的父类 A 对象试试 val l1: ArrayList<out B> = arrayListOf(A(), B(), C(), D()) // error // 这里就会报错, 无法添加高于B的对象
虽然可以这么写, 但最好别这么用, 协变在调用函数传参的时候才能得到充分的体现
-
协变无法添加元素
val l0: ArrayList<out B> = arrayListOf(B()) l0.add(C()) // error, 无法再次添加对象
再次声明:
val l0: ArrayList<out B> = arrayListOf(B())
虽然运行这么写, 但最好不要这么用 -
看下面的
f1
和f2
函数
但是可以这么传递:
逆变(contravariant
): 相反的子类关系
正常情况下, Animal
是 Cat
的父类, Animal
的子类型是 Cat
, List<Animal>
也是List<Cat>
的子类型, 这是协变, 但如果 List<Cat>
是List<Animal>
的子类型的话, 这种子类型关系逆反了, 这就是逆变
研究逆变需要了解两个步骤
初始化阶段
初始化阶段
List<in Cat>
可以看作是Any
类型使用阶段
在使用的时候,
in Cat
变成了List<Cat>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BxtKzdHe-1656299835954)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f8d2b506bc144e79d5ecbf3c1576f4b~tplv-k3u1fbpfcp-watermark.image?)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fZUvV7eJ-1656299835955)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/78e718b43c9e420bb1f14103caab0f5a~tplv-k3u1fbpfcp-watermark.image?)]
kotlin 中的逆变
同样的 kotlin 支持:
1. 类的泛型参数逆变
2. 函数集合参数泛型逆变
类的泛型参数逆变
class A<in T> {
fun write(t: T) {
}
}
函数集合参数泛型逆变
逆变在调用函数并传参的时候得到体现, 而在使用逆变后的对象添加参数时, 又恢复了 父类指针指向子类对象的赋值兼容性原则
协变逆变的总结
父类 ==> 当前类 ==> 当前类子类
|–> 👆 -->
👆 这里就是上界的边界
协变: 规定了泛型(类型)上界(上边界), 该上限限定了只能传递某个类型及该类型的子类()
父类 ==> 当前类 ==> 当前类子类
Any --> 👆 <–|
👆 这里就是下界的边界
逆变: 规定了下界(下边界), 规定了只能传递某个类型及该类型的父类
在 kotlin 中, 如果泛型被标记为
out
, 则该泛型只能调用符合泛型out
位置的函数, 比如fun get() : T
, 如果泛型被标记为in
, 那么只能调用该类的复合in
位置的函数比如:fun add(t: T): void
out
协变, 只读,in
逆变, 能读写
使用协变和逆变写个 copyData
函数
- 普通方式实现该函数
fun <T> copyData01(source: MutableList<T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
- 使用约束的方式实现该函数
/**
* T 是 R 的子类或者 T 就是 R, 记作: T <= R
* 所以 source: MutableList<T> 是子类集
* destination: MutableList<R> 是父类集
* 把子类集source的 item 依次给 父类集的 destination
*/
fun <T : R, R> copyData02(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
// T 是子类(source)
// R 是父类(destination)
// R ==> T 父类 指向 子类
// destination ==> source 父类 指向 子类
destination.add(item)
}
}
这种方式不好左区分, 到底哪个是父类, 哪个是子类, 哪个是输出, 哪个是输入
- 使用协变的方式实现函数
/**
* 对读取函数使用 out 泛型修饰符
* out T 表示 T 或者 T 的子类
*/
fun <T> copyData03(source: MutableList<out T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
/**
* in T: T 的父类
*/
fun <T> copyData04(source: MutableList<T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
/**
* 下面这就是声明处变型
*/
fun <T> copyData05(source: MutableList<out T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
/**
* List 本身就是只读的, 所以看 List 源码的话会看到 public interface List<out E> 这段代码
* 看到 out E 了么?
*/
fun <T> copyData06(source: List<T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
source: MutableList<out T>, destination: MutableList<in T>
这种方式能够很明显的发现哪个是输出, 哪个是输入
泛型类中 out
和 in
的位置
kotlin
支持在 类声明 处定义泛型的 变型 , 也支持像 java 一样在 函数位置写上 变型
星号投影: 使用 *
代替类型参数
- 星号投影不清楚存入的类型到底是哪个, 所以一般不做写入, 仅作读取
所以功能上类似于 List<out Any?>
, 在没有任何类型信息的情况下, Any
是最好的选择
- 使用星号投影的, 说明开发者并不需要知道读取出来的泛型具体是什么类型
}
}
/**
- List 本身就是只读的, 所以看 List 源码的话会看到 public interface List 这段代码
- 看到 out E 了么?
*/
fun copyData06(source: List, destination: MutableList) {
for (item in source) {
destination.add(item)
}
}
> `source: MutableList<out T>, destination: MutableList<in T>` 这种方式能够很明显的发现哪个是输出, 哪个是输入
### 泛型类中 `out` 和 `in` 的位置
[外链图片转存中...(img-7OKXBlNQ-1656299835959)]
`kotlin` 支持在 类声明 处定义泛型的 变型 , 也支持像 java 一样在 函数位置写上 变型
### 星号投影: 使用 `*` 代替类型参数
1. 星号投影不清楚存入的类型到底是哪个, 所以一般不做写入, 仅作读取
所以功能上类似于 `List<out Any?>`, 在没有任何类型信息的情况下, `Any` 是最好的选择
2. 使用星号投影的, 说明开发者并不需要知道读取出来的泛型具体是什么类型
> 说白一点, 星号投影把它当作 `out Any?` 吧, 读取出来的对象当作 `Any?` 对象就行, 不能写入