第十四讲 Kotlin泛型篇之真泛型
前介
上一篇,我们算是把 通配符 和 泛型 弄你明白了吧!这一篇我们讲解 Kotlin 泛型和 Java 变化之处 真泛型。
真泛型
上一篇章节,我们花费了很长时间和大家证明泛型在编译后是被擦除调的,也就是其实泛型在 Java 的世界中是不存在的,只在编写代码期间存在的一个约束。
既然有擦除,那就存在一个问题,我们无法判断传入的泛型类型或通过泛型创建对象。

public static <T extends Number> void check(T value){
/**
* 我想确定 T 的类型 或 创建对象 或 获取 T的字节码
*/
// 编译器报错
T tmp = new T();
// 报错
Class<T> tClass = value.class;
}
我们再来看看 Kotlin 中的,这种代码报错吗?
fun <T : Number> check(value: T) {
/**
* 我想确定 T 的类型 或 创建对象 或 获取 T的字节码
*/
// 编译器报错
val tmp = T()
// 报错
val tClass = value::class.java
}
最终不管
Java还是Kotlin都是报错的。大家思考下为啥报错呢?其实就和泛型擦除有关系哦,因为泛型擦除的原因,只有在代码运行到check方法,才知道传入的类型是什么,编译期间根本不知道传入的类型是什么。就算通过调用点知道了传入的类型,但check方法只编译了一个,也解决不了这个问题。
Kotlin 的真泛型,就是为了解决这样的问题。试想下 Java 解决不了真正原因是泛型擦除和泛型方法只编译了一个。哪 Kotlin 有没有解决的办法呢?
还记得函数篇章讲解的 内联函数 吗?内联函数 不就会将代码平铺到调用点吗?不就代表这个泛型方法编译 N 个,Kotlin 通过调用点知道了传入的类型。然后通过 内联函数 将代码平铺到调用点,泛型方法中的代码不就知道了泛型的类型啦(我C吊炸天的想法)。

所以说要想定义 真泛型 一定要是内联函数,并且通过 reified 修饰泛型。
fun main() {
check(3)
check(3.0)
}
/**
* 吊炸天的想法
* 真泛型
*/
inline fun <reified T : Number> check(value: T) {
/**
* 看能获取类的 class 字节码啦
*/
val java = T::class.java
}
注意:真泛型虽然知道了泛型的类型,但是也不能直接使用
T()构造对象,为啥呢?其实想想也很简单,鬼知道你传入的类型有没有无参构造函数呢?
接下来我们看看,反编译后的 Java 代码是什么样的。

实战
我们理解了真泛型了,以及真泛型是怎么实现的。有什么用呢?其实用处大了。我这里就给大家讲解一个我项目中的实例吧。
需求:我们要连接一个 WebSocket ,服务端会主动为我们 push 消息,对应用一个 action 区分动作,对应有一个 data 字段使用 json 来传递数据,但是 data 的 json 的结构是不同的哦。
那我们设计肯定有有一个 WebSocket 管理类,可以通过 addAction 来增加时间类型。按照以前的想法我们会如何设计呢?
/**
* 封装下类型
*/
data class ActionType(val dataClzz: Class<*>, val callBack: (Any) -> Unit)
object SocketManager {
val actions = mutableMapOf<String, ActionType>()
val gson = Gson()
/**
* 收到事件
*/
fun onMessage(action: String, data: String) {
actions[action]?.apply {
// 解析数据
val fromJson = gson.fromJson(data, dataClzz)
callBack(fromJson)
}
}
/**
* 增加回调
*/
fun <T> addHandler(action: String, dataClzz: Class<T>, callBack: (T) -> Unit) {
/**
* 记录时间
*/
actions[action] = ActionType(dataClzz, callBack as (Any) -> Unit)
}
}
/**
* 测试数据
*/
data class LoginResult(val userName: String, val userID: Long)
fun main() {
/**
* 注册action回调
* 注册还需要填写一个 clazz 的参数,原因就是泛型函数,中的泛型会被擦除,不能知道类型
*/
SocketManager.addHandler<LoginResult>("login", LoginResult::class.java) {
println("得到回调数据:$it")
}
/**
* 模拟发送数据
*/
SocketManager.onMessage("login", Gson().toJson(LoginResult("阿文", 18)))
}
通过上方的实例代码,我们调用 addHandler 还需要传入一个 Class 的对象。通过真泛型就可以省略调这个参数。
只需要修改 addHandler 方法。
/**
* 增加回调
*/
inline fun <reified T> addHandler(action: String, noinline callBack: (T) -> Unit) {
/**
* 记录时间
*/
actions[action] = ActionType(T::class.java, callBack as (Any) -> Unit)
}
这里有个以前讲过的
内联函数的知识点,若函数通过inline修饰,默认传入的Lambda的代码也会被平铺。由于我们的lambda表达式,并不是在addHandler方法中调用,所以一定要用noinline修饰Lambda表达式。
接下来我们使用就简单的多啦!
fun main() {
/**
* 注册action回调
* 注册还需要填写一个 clazz 的参数,原因就是泛型函数,中的泛型会被擦除,不能知道类型
*/
SocketManager.addHandler<LoginResult>("login") {
println("得到回调数据:$it")
}
/**
* 模拟发送数据
*/
SocketManager.onMessage("login", Gson().toJson(LoginResult("阿文", 18)))
}
当然这个例子很简单,大家可以探索更多的用处了。总的来说就是说我们能在泛型方法中,获取泛型的类型了。如果想要创建对象可以用反射或者抽取一个基类,约定泛型的类型。
真泛型,只能用在泛型方法中,不能用在类里。
总结
这章我们主要讲解了下真泛型,其实 Kotlin 泛型和 Java 发现不同还有一些,但是用处都不是很多。例如:Kotlin 的类上也支持使用 out 和 in 关键词(上界和下界),有兴趣大家可以去看看,其实就是约束条件。
/**
* out上界修饰 T
*/
class Test<out T : Number> {
/**
* 不能改
* 代码报错
*/
fun add(v: T) {
}
/**
* 可以用
*/
fun get(): T? {
return null
}
}