九、kotlin的泛型

kotlin的泛型基础和 java 很像, 所以我建议学习 kotlin 的泛型前, 先去学习下 java 的泛型, 至少搞懂通配符, <? extends X><? super X> 是怎么回事, 怎么写 泛型函数, 泛型类, 知道泛型的本质是什么?

泛型

对于我来说, 我从c++转过来的, 在 c++ 中并没有什么泛型, 只有模板, c++ 根据编译期间所遇到的类型, 按照模板生成代码, 每种类型的模板生成一段代码(不知道现在还是不是这样实现, 我只学到 c++11), 但从中我体会到泛型其实不过和函数参数一样, 函数的() 内传递的是参数, 而泛型的 <> 传递的是类型. 说白了都是参数, 只不过一个是变量另一个是类型

函数, 参数, 属性和类的泛型

fun <T> print(t: T) {
   println(t)
}

class GenericsDemo01<T>(val f: T) {
   fun print(t: T) {
      println(t)
   }
}
复制代码

泛型约束

很多时候我们需要将泛型的类型约束在某个界限, 比如: 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 类型ab 做加法运算符这样尴尬的事情

泛型约束不会像集合泛型约束那样严格控制 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 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 多出很多约束类的函数(包括扩展函数等)

image.png

endsWith 函数是 CharSequence 的扩展函数, appendAppendable 接口的函数

前面的 sum 函数也是

image.png

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 一样, 泛型在运行时类型擦除, 在编译期间存在类型, 在 运行期间当作 Any 就好

fun <T> isIntList(list: List<T>) {
   if (list is List<Int>) { // 这里报错
      println("这是错误的")
   }
}

fun main() {
   val list = listOf(1, 2, 3)
   isIntList(list)
}
复制代码

image.png

但是可以这样:

if (list is List<*>)
复制代码

kotlin 编译器可以判断在同一个作用域内的泛型类型

val list = listOf(1, 2, 3)
if (list is List<Int>) {
    println("这样是可以的")
}
复制代码

下面这种情况也会出现问题

image.png

实化类型参数 reified T

在运行期间, 类型被当作 Any 类型, 但它想被强制转换成 T 类型明显是不行的, 这种情况下, 可以考虑使用 inlinereified 配合实现

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() {
   val loadService = loadService<Int>()
}
复制代码

变型: 泛型和子类型化

类、类型和子类型

类和类型的区别

在很多情况下, 类都可以大体上当作类型, 但实际上, 类和类型不是一个东西就比如: 空类型和非空类型, IntInt? , 请确认下 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 则可以说 IntNumber子类型, 而同时我们可以说 NumberInt超类型

简单点: 子类的超类型是父类, 父类的子类型是子类, 只要记住这种关系就好

协变和逆变

高端的概念总会有落地的实现, 我们学习要达到的程度是用最简单的一句话描述这些概念

协变(covariant)

  1. 是什么?

协变: 是一种关系, 一种父类引用 指向 子类对象 的恒定关系

  • Number 引用总能够指向 Int 对象, 那么 Number 的子类型是 Int , 则 NumberInt 是协变的

  • 那么同样的 Number[] 引用 总能够 指向 Int[] , 则 Number[]Int[] 是协变的

  • 同样的, List<Number> 的引用总能够 指向 List<Int> 那么我们也能够说: List<Number>List<Int> 有协变关系

但, java 因为历史关系, 使用了类型擦除技术, 所以 任何类型变到泛型的话, 就不会有所谓的协变(逆变)关系, 因为到了运行时期 java 总把类型变成 List<Object> 或者 直接是 List 类型, 如果强制开出协变关系, 则会出现一些安全问题

如果 java 泛型支持协变存在的问题

如果泛型支持协变的话, 会导致下面的问题:

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(但实际上这里不会编译通过的)
objeList.add(10.9); // 这里在运行期间将会编译通过, 运行通过, 因为还是 父类引用指向子类的对象, object --> double
复制代码

上面这段集合中, 如果集合同时满足协变和类型擦除的话, 就会编译通过, 运行也将不会报错, 因为在运行期间, 类型类彻底擦除成 Object

这是不允许的, 所以它不支持协变

为了解决上面的问题, java 引入了属于 java 的泛型的协变

java泛型对于"消失的协变关系"的解决方案

协变关系, 又有人叫 子类型关系

java 引入了 通配符?, 然后用 List<? extends Number> 表示协变, 相当于没有类型擦除List<Number>, 接受NumberNumber的子类存入List<Number> 集合中

所以 List<? extends Number> 集合可以存入

image.png

上面这些类的对象

那么他是如何解决的上面那个问题的呢?

答: java 的解决方法很简单, 一刀切, 如果类型是 <? extends Number> 协变的, 那么他就不允许写入, 修改等操作. 只允许读取

image.png

我特么, 解决不了问题, 就解决提出问题的人是吧???

小总结: List<? extends Number> 不好记里面可以存放什么类, 可以直接认为是 支持协变的 List<Number> 理解就好了, 支持协变的话, Number 集合可以存入它和它的子类, 这种情况有人称之为 上界, 因为它限制了类族的上限

对应于 kotlin 的协变关系

在 kotlin 中, 协变将会是: 1. 在类处类型参数协变 2. 在函数处集合泛型的协变

类处类型参数的协变
interface Producer<out T> {
    fun produce() : T
}
复制代码

out 放在那里的位置, 主要有两个功能:

  • 子类型将会被保留(Producer<Cat>Producer<Animal>的子类)
  • T 只能用在 out 位置

image.png

in 的位置在函数参数, out 位置在函数返回值, 既是in又是out则不需要标记, 同样的 out 标记的泛型只能读取, 不能写入, in标记的泛型只能写入不能读取(和java优点不太一样???)

  1. 构造函数的参数使用 T 为泛型既不在 in 也不在 out, 但如果参数前面加上了 val/var 则需要注意 in/out 了, 因为 它不再是参数而是属性, 属性有 setter/getter

  2. MutableList不能使用out

  3. 协变后的集合不允许写入, 只允许读取

  4. 协变的out B表示只能填入B或者B的子类

函数处集合泛型的协变

和 java 类似的用法

out T 对应了 java 的 ? extends T

in T 对应了 java 的 ? super T

private fun f(list: ArrayList<out B>) {
   // 使用时不允许写入
// list.add(D()) // 参数变成 Nothing 了, 所以不能添加 D() 对象
   list.forEach {
      println(it.javaClass)
   }
}
复制代码

逆变(contravariant): 相反的子类关系

正常情况下, AnimalCat 的父类, Animal 的子类型是 Cat , List<Animal>也是List<Cat>的子类型, 这是协变, 但如果 List<Cat>List<Animal>的子类型的话, 这种子类型关系逆反了, 这就是逆变

image.png

image.png

kotlin 中的逆变

同样的 kotlin 支持: 1. 类的泛型参数逆变 2. 函数集合参数泛型逆变

类的泛型参数逆变
class A<in T> {
   fun write(t: T) {
   }
}
复制代码
函数集合参数泛型逆变
open class A
open class B : A()
open class C : B()
open class D : C()

fun c(list: ArrayList<in C>) {
// list.add(A()) // 泛型使用时, 可以直接当成 ArrayList<C>
// list.add(B()) // 泛型使用时, 可以直接当成 ArrayList<C>
   list.add(C()) // 符合父类引用指向子类对象
   list.add(D()) // 符合父类引用指向子类对象
   list.forEach {
      println(it::class.java)
   }
}

fun main() {
   val l1: ArrayList<A> = arrayListOf(A(), B())
   val l2: ArrayList<D> = arrayListOf(D())
   // val l3: ArrayList<in C> = arrayListOf<D>(D()) // 报错
   val l4: ArrayList<in C> =
      arrayListOf(D()) // arrayListOf(D()) 是 ArrayList<C> 类型, kotlin编译器做了恶心的优化, 提高了泛型的类型, 从D变成了C, 所以不会报错
   c(l1) // 符合  Any ~ C类型
// c(l2) // 错误, 只能接受 Any 到 C 范围内的泛型, 实际是 ArrayList<in C>(Any ~ C类型) 指向 ArrayList<D>
   c(arrayListOf(D())) // 类型是 ArrayList<C>, 所以不会报错
}
复制代码
  1. 逆变初始化时(实参赋值给形参时in C), 代表一个范围的类型 [Any, C] 没有具体指向某个类型, 是一个宽泛的类型, 这是逆变主要存在的作用

  2. 逆变使用时, 把它当作ArrayList<C>就好了. 把要写入的元素限定在C的子类, 限定C函数也会多保留些, 也不会很多争议说为什么不限定为Any还是A又或者是B, C

防止出现限定为 B, 写入为 A 的存在, 一刀切, 这样就为满足赋值兼容性原则(父类引用指向子类对象)

  1. 逆变后的集合只能写入, 不能读取, 读取的话类型会消失, 他会返回[Any, C]中的任何一个可能的类型, 所以直接返回了Any, 类型消失了

至于为什么我要说要满足赋值兼容性原则原则? 我想说, 逆变说的好听叫逆子类型关系, 其实说到底是一个 保证一种类型在一个范围里面罢了, 比如:

fun c(list: ArrayList<in C>) {
    list.add(C())
    list.add(D())
    list.forEach {
        println(it::class.java)
    }
}

fun main() {
    val l: ArrayList<A> = arrayListOf(A())
    c(l)
}
复制代码

看起来是 C -> A, 子类引用指向了父类对象, 其实何尝不是[Any, C] -> A 这样的呢? 一个类型集合 -> A类, C 不行, Any -> A总行了吧? 还是符合赋值兼容性原则

使用协变和逆变写个 copyData 函数

  1. 普通方式实现该函数
fun <T> copyData01(source: MutableList<T>, destination: MutableList<T>) {
   for (item in source) {
      destination.add(item)
   }
}
复制代码
  1. 使用约束的方式实现该函数
/**
 * 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) {
      destination.add(item)
   }
}
复制代码
  1. 使用协变的方式实现函数
/**
 * 对读取函数使用 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)
   }
}
复制代码

在 kotlin 中, 如果泛型被标记为 out, 则该泛型只能调用符合泛型 out 位置的函数, 比如fun get() : T, 如果泛型被标记为 in, 那么只能调用该类的复合 in 位置的函数比如: fun add(t: T): void

out 协变, 只读, in 逆变, 能读写, 但写会失去类型, 没有 in/out 不变

泛型类中 outin 的位置

image.png

kotlin 支持在 类声明 处定义泛型的 变型 , 也支持像 java 一样在 函数位置写上 变型

星号投影: 使用 * 代替类型参数

  1. 星号投影不清楚存入的类型到底是哪个, 所以一般不做写入, 仅作读取

所以功能上类似于 List<out Any?>, 在没有任何类型信息的情况下, Any 是最好的选择

  1. 使用星号投影的, 说明开发者并不需要知道读取出来的泛型具体是什么类型

说白一点, 星号投影把它当作 out Any? 吧, 读取出来的对象当作 Any? 对象就行, 不能写入

猜你喜欢

转载自juejin.im/post/7019280743381762056