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 | class Box<T>(t: T) { |
泛型方法,与 java 类似的可以限制 T
的上下界.多重约束时使用 where,在 java 中则是 <T extend a & b>
.
1 | fun <T> boxIn(value: T) = Box(value) |
泛型接口类似,不再重复.
型变
java 泛型使用时有一种限制是,子类集合的类型,不是父类集合的子类,说人话就是 Integer
是 Number
的子类,但是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 | List<? extend Number> //(java) 这里的 extend 与 T extend xx,含义不同,千万注意. |
java 中协变只能在使用时声明协变,即 ? extend
后面需要是一个具体的类型(只能如上面的示例),不能是泛型的符号 T
.但是 kotlin 中 out
后面可以接泛型类型,即 kt 允许声明时协变.
1 | interface Test<out T> {...} |
逆变
与协变相反的是逆变.List<? super Integer>
可以传入 List<Number>
.这样 List<Number>
是 List<? super Integer>
的子类,但是 Number 却是 Integer 的父类.这样的转换称为逆变.
与协变类似的限制是,例如声明 List<? super Integer>
,这样可以传入 List<Number>
类型,传入的实例只能写不能读取任何属性.
声明: kt 的in更简单一点,即传入的实例只写不读.
1 | List<? super Integer> //java ,这里super 与 T super xx 含义不同. |
同样的,kt 中支持声明时逆变,java 不支持.
1 | fun<in T> test(){} //可以 |
星投影
java 中可以使用单个 ?
,相当于 ? extend Object
.
kotlin 中类似的写法是 *
,相当于 out Any
,但如果你的类型声明中已经有了 out 或 in,这个限制在变量声明时依然存在.
1 | interface Test<out T:Number>{...} |
"真"泛型
kotlin 和 java 的泛型都是会执行编译期的类型擦除,即除了型变的情况外,泛型实例在内存中是 object
类型而不是代码中我们传入的类型.
因为类型擦除的特性,相对于能在运行时拿到具体类型的真泛型而言,java 的泛型有时被称为"假"泛型.那 kotlin 在 android/jvm 的实现必须与 java 兼容,因此 kotlin 的泛型也会有类型擦除,但是 kt 还留了一手,通过声明 reified
就能实现"真"泛型.
java 因为类型擦除的原因,在有需要知道泛型类型的情况下,无法操作,变通一点是可以传入一个 Class<T>
类型的参数.但是 kt 中可以通过 reified
解决这个问题.
1 | <T> void check(Object item, Class<T> type) { |
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
2inline fun <reified T> Gson.fromJson(json: String) = fromJson(json, T::class.java)
val user: User = Gson().fromJson(json)
其他
上文提到了,"真"泛型无法直接实例化一个对象.但是我们已经可以拿到泛型对应的 Class类了,因此可以通过反射可以绕过这个限制.
默认无参构造方法,最简单的 newInstance()
,最终是调用那个无参的构造方法返回一个实例.也可以通过先获得那个无参的构造方法,择机调用.
1 | val t: T = T::class.java.newInstance() |
有参数的构造方法,这个需要你知道有参的构造方法具体是那些参数,然后才能获取对应的构造函数.
1 | // 可以通过反射获取一组参数的 type,传入 getConstructor/getDeclaredConstructor 获取构造方法,再传入参数,调用构造方法. |
getConstructor/getDeclaredConstructor 区别
- getConstructor 只返回 public 的构造方法.
- getDeclaredConstructor 会返回所有,包括 privacy.使用私有构造方法有时需要设置
isAccessible = true