kotlin 泛型

  • kotlin 泛型及"可以实例化的真泛型"

  • 资料来源:

    https://www.kotlincn.net/docs/reference/generics.html
    https://kaixue.io/kotlin-generics/
    https://www.runoob.com/kotlin/kotlin-generics.html

  • 更新

    1
    20.06.20 初始化

导语

泛型一直用的很多了,在使用中也有不少新的体会,故重新总结到此.

基本概念

参照维基有两种定义

  • 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象.(这是当今较常见的定义)
  • 在程序编码中一些包含参数的类,其参数可以代表类或对象等等.(现在人们大多把这称作模板)

kotlin/java 中是前一种,即泛型只代表类.kotlin 的泛型在 jvm 上实现与 java 泛型有很大联系,故下文会将两者类似的语法放在一块对比.

基础

使用泛型的地方无非是类/方法/接口.

泛型类,声明泛型类与 java 没差别,使用时需要带上具体的类型.

1
2
3
4
class Box<T>(t: T) {
var value = t
}
val box: Box<Int> = Box<Int>(1)

泛型方法,与 java 类似的可以限制 T 的上下界.多重约束时使用 where,在 java 中则是 <T extend a & b>.

1
2
3
4
5
6
fun <T> boxIn(value: T) = Box(value)
fun <T : a> boxIn(value: T) = Box(value)
fun <T> boxIn(value: T)
where T : a,
T : b
{ Box(value) }

泛型接口类似,不再重复.

型变

java 泛型使用时有一种限制是,子类集合的类型,不是父类集合的子类,说人话就是 IntegerNumber 的子类,但是List<Integer>List<Number> 没有继承关系.但有时我们又需要在 子类集合 与 父类集合 之间建立联系,而这个关联的操作就是型变.

一个例子: 我有一堆传感器采集数据,我想有一个全局的数组存放这些数据,结果俩不同牌子都是采集温度的传感器,一个传回来是 List<Integer> 一个是 List<Long>.kt 中没有隐式的转换,我不想太麻烦,就声明了一个全局的 List<Number> 实例,但是 List<Integer> 是无法直接添加到 List<Number> 实例的,这里就需要用到型变了.

型变有两种: 协变 和 逆变,又分 使用时型变 和 声明时型变.

协变

上文例子就算协变.即子类集合类型是父类集合类型的子类.List<Integer> 可以作为 List<Number> 的子类.

java/kotlin 的协变有一个限制是,只读不可写.即我可以使用 List<Integer> 添加到 List<Number> 中,却不可以使用专属于 List<Integer> 的方法和属性.

声明: kotlin 的 out 更加易懂一点,即传入的实例只作为输出,只读.

1
2
List<? extend Number> //(java) 这里的 extend 与 T extend xx,含义不同,千万注意.
List<out Number> //(kotlin)

java 中协变只能在使用时声明协变,即 ? extend 后面需要是一个具体的类型(只能如上面的示例),不能是泛型的符号 T.但是 kotlin 中 out 后面可以接泛型类型,即 kt 允许声明时协变.

1
2
interface Test<out T> {...}
interface Test<? extends T> { ... } //java 不允许

逆变

与协变相反的是逆变.List<? super Integer> 可以传入 List<Number>.这样 List<Number>List<? super Integer> 的子类,但是 Number 却是 Integer 的父类.这样的转换称为逆变.

与协变类似的限制是,例如声明 List<? super Integer>,这样可以传入 List<Number> 类型,传入的实例只能写不能读取任何属性.

声明: kt 的in更简单一点,即传入的实例只写不读.

1
2
List<? super Integer> //java ,这里super 与 T super xx 含义不同.
List<in Number> //kotlin

同样的,kt 中支持声明时逆变,java 不支持.

1
2
fun<in T> test(){} //可以
public <? super T> void test(){...}//java 中不允许

星投影

java 中可以使用单个 ? ,相当于 ? extend Object.

kotlin 中类似的写法是 * ,相当于 out Any,但如果你的类型声明中已经有了 out 或 in,这个限制在变量声明时依然存在.

1
2
interface Test<out T:Number>{...}
var tmp : Text<*> = ... //这里的 * 仅被视为 out Number

"真"泛型

kotlin 和 java 的泛型都是会执行编译期的类型擦除,即除了型变的情况外,泛型实例在内存中是 object 类型而不是代码中我们传入的类型.

因为类型擦除的特性,相对于能在运行时拿到具体类型的真泛型而言,java 的泛型有时被称为"假"泛型.那 kotlin 在 android/jvm 的实现必须与 java 兼容,因此 kotlin 的泛型也会有类型擦除,但是 kt 还留了一手,通过声明 reified 就能实现"真"泛型.

java 因为类型擦除的原因,在有需要知道泛型类型的情况下,无法操作,变通一点是可以传入一个 Class<T> 类型的参数.但是 kt 中可以通过 reified 解决这个问题.

1
2
3
4
5
6
7
8
9
10
11
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
System.out.println(item);
}
}

inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) {
println(item)
}
}

kotlin 编译后依旧是跑在 jvm 上,kt 是怎么做到的? 花样就在 inline 上面,简而言之上面的代码编译再反编译成 java 代码,并不包含泛型.编译时 kt 的编译器就将泛型替换成了实际的类型,没有了泛型,运行时自然能得到参数的实际类型.(不知道理解的对不对),具体可以参照 Kotlin的独门秘籍Reified实化类型参数-上篇Kotlin的独门秘籍Reified实化类型参数(下篇).

注意: reified 只能用于内联函数.且有别的限制.

  • java 中无法调用带有 reified 的 kt 函数.(单纯的内联函数也有限制,java 可以调用,但是会失去内联的效果)
  • 参数使用限制
    • 不能使用 非实化类型形参 作为类型实参 调用带实化类型参数 的函数.(???)
    • 不能使用 实化类型参数 创建实例对象.(可以用反射绕过)
    • 不能调用 实化类型参数 的伴生对象方法
    • reified 关键字只能用于内联函数,不能作用与类和属性.

一些应用

  • 直接判断变量是不是 泛型的类型,示例如上.

  • 传入泛型的类型

    • json 反序列化时通常需要对象的类型,例如 User user = new Gson().fromJson(getJson(), User.class);

    • 但在 kt 中就能玩一手.

      1
      2
      inline fun <reified T> Gson.fromJson(json: String) = fromJson(json, T::class.java)
      val user: User = Gson().fromJson(json)

其他

上文提到了,"真"泛型无法直接实例化一个对象.但是我们已经可以拿到泛型对应的 Class类了,因此可以通过反射可以绕过这个限制.

默认无参构造方法,最简单的 newInstance(),最终是调用那个无参的构造方法返回一个实例.也可以通过先获得那个无参的构造方法,择机调用.

1
2
3
4
5
6
7
val t: T = T::class.java.newInstance()
//or
val c = T::class.java.getConstructor()
val t: T = c.newInstance()
//or
val c = T::class.java.getDeclaredConstructor()
val t: T = c.newInstance()

有参数的构造方法,这个需要你知道有参的构造方法具体是那些参数,然后才能获取对应的构造函数.

1
2
3
4
5
6
// 可以通过反射获取一组参数的 type,传入 getConstructor/getDeclaredConstructor 获取构造方法,再传入参数,调用构造方法.
val c = T::class.java.getConstructor(listType)
val t: T = c.newInstance(list)
//or
val c = T::class.java.getDeclaredConstructor(listType)
val t: T = c.newInstance(list)

getConstructor/getDeclaredConstructor 区别

  • getConstructor 只返回 public 的构造方法.
  • getDeclaredConstructor 会返回所有,包括 privacy.使用私有构造方法有时需要设置 isAccessible = true