kotlin-泛型篇之真泛型

3,057 阅读4分钟

第十四讲 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 来传递数据,但是 datajson 的结构是不同的哦。

那我们设计肯定有有一个 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 的类上也支持使用 outin 关键词(上界和下界),有兴趣大家可以去看看,其实就是约束条件。

/**
 * out上界修饰 T
 */
class Test<out T : Number> {

    /**
     * 不能改
     * 代码报错
     */
    fun add(v: T) {
    }

    /**
     * 可以用
     */
    fun get(): T? {
        return null
    }
}