第十四讲 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
}
}