5Kotlin in Action

Kotlin类型系统

类型系统

空类型泛型型变星投影

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第5章:Kotlin类型系统(Kotlin Type System)

一、导读

1.1 本章学习目标

第5章深入讲解 Kotlin 的类型系统——这是 Kotlin 相比 Java 最显著的改进之一。Kotlin 的类型系统不仅提供了空安全,还在泛型、型变、星投影等方面做出了创新性的设计。

本章的核心学习目标包括:

  • 深入理解空类型系统:可空类型、安全调用、平台类型、类型别名
  • 掌握泛型的完整用法:泛型函数、泛型类、泛型约束
  • 理解型变(Variance):声明处型变(in/out)和使用处型变(星投影)
  • 掌握类型擦除与 reified 类型参数
  • 学会使用类型别名(typealias)提高代码可读性

1.2 为什么本章如此重要

类型系统是 Kotlin 的核心竞争力。Java 的类型系统经过几十年的发展,积累了许多历史包袱——协变数组的类型安全问题、缺乏空类型区分、泛型的类型擦除限制等。Kotlin 在设计类型系统时,既保留了与 Java 的兼容性,又引入了现代化的改进。

空类型系统让你在编译期就发现潜在的空指针异常,而不是在生产环境中。泛型的型变系统让集合操作的类型安全性更加精确。reified 类型参数突破了类型擦除的限制,让你可以在泛型函数中使用 is 检查。

1.3 核心问题引导

为什么 Kotlin 的 List<String> 不是 List<Any> 的子类型?这与 Java 有什么不同?

out 和 in 关键字在泛型中的作用是什么?它们与 Java 的通配符 ? extends 和 ? super 有什么对应关系?

什么是星投影(star projection)?在什么场景下需要使用它?

reified 类型参数是如何突破类型擦除限制的?它有什么限制?

平台类型(platform type)String! 的含义是什么?为什么 Kotlin 不对 Java 代码强制空检查?


二、核心概念详解

2.1 空类型系统深入

2.1.1 可空类型的安全操作

Kotlin 提供了丰富的空安全操作符:

kotlin
val name: String? = getName()

// 安全调用 ?.
val length: Int? = name?.length

// Elvis 操作符 ?:
val lengthOrDefault: Int = name?.length ?: 0

// 安全调用链
val city: String? = person?.company?.address?.city

// let + 安全调用
name?.let { n ->
    println("Name is $n with length ${n.length}")
}

// 可空类型的集合操作
val list: List<String?>? = getList()
val nonNullCount = list?.count { it != null } ?: 0

2.1.2 平台类型(Platform Type)

当 Kotlin 调用 Java 代码时,Java 的引用类型被映射为"平台类型" T!:

kotlin
// Java 方法: public String getName() { return name; }
// 在 Kotlin 中,返回类型是 String!(平台类型)
val name: String! = javaObject.getName()

// 平台类型可以当作可空或非空使用
val nonNull: String = javaObject.getName()    // 如果实际为 null,运行时 NPE
val nullable: String? = javaObject.getName()  // 总是安全的

平台类型是 Kotlin 对 Java 可空性未知的妥协。Kotlin 不会对 Java 返回值强制空检查(因为那会破坏互操作性),但会在你将其赋值给非空类型时插入运行时检查。

2.1.3 类型别名(Type Alias)

kotlin
typealias UserId = Long
typealias UserMap = Map<UserId, String>
typealias Predicate<T> = (T) -> Boolean

fun findUser(id: UserId): String? = userMap[id]

// 类型别名不是新类型,只是语法糖
// UserId 和 Long 是完全相同的类型
val id: UserId = 42L
val longId: Long = id    // 完全合法

2.2 泛型

2.2.1 泛型函数

kotlin
fun <T> List<T>.secondOrNull(): T? = when {
    size < 2 -> null
    else -> this[1]
}

fun <T : Comparable<T>> List<T>.sorted(): List<T> {
    return this.toMutableList().apply { sort() }
}

2.2.2 泛型类与类型约束

kotlin
// 单个上界
class Repository<T : BaseEntity> {
    fun findById(id: Long): T? { /* ... */ }
}

// 多个上界(使用 where 子句)
fun <T> copyIfAssignable(item: T)
    where T : Comparable<T>, T : Serializable {
    // item 既是 Comparable 又是 Serializable
}

// 泛型类的约束
class NumberBox<T : Number> {
    fun sum(a: T, b: T): Double = a.toDouble() + b.toDouble()
}

2.2.3 类型擦除与 reified

Kotlin 和 Java 一样,泛型在运行时会被擦除。但 Kotlin 的 inline 函数可以使用 reified 类型参数来突破这个限制:

kotlin
// 没有 reified:无法使用 is 检查泛型类型
// fun <T> isInstance(obj: Any): Boolean {
//     return obj is T    // 编译错误!
// }

// 使用 reified
inline fun <reified T> isInstance(obj: Any): Boolean {
    return obj is T    // OK!
}

// 实际应用
inline fun <reified T> Gson.fromJson(json: String): T {
    return this.fromJson(json, T::class.java)
}

val user: User = gson.fromJson(jsonString)
// 编译器自动推断 T 为 User,reified 让运行时能获取 User::class.java

reified 的限制:

  • 只能在 inline 函数中使用
  • 不能使用 reified 类(只有函数可以)
  • 不能创建类型参数的实例(T() 仍然不合法)

2.3 型变(Variance)

2.3.1 为什么需要型变

kotlin
// 这个函数能编译吗?
fun addInt(list: MutableList<Any>) {
    list.add(42)
}

val strings: MutableList<String> = mutableListOf("a", "b")
addInt(strings)    // 如果允许,strings 中就有了 Int!类型不安全!

Kotlin 的 MutableList<Int> 不是 MutableList<Any> 的子类型,这是正确的——因为 MutableList 是可变的,允许向其中添加元素。

2.3.2 声明处型变

Kotlin 使用 out 和 in 关键字在声明处指定型变:

kotlin
// out: 生产者,只能产出 T
interface Producer<out T> {
    fun produce(): T
    // fun consume(item: T)    // 编译错误!out 参数位置不能使用 T
}

// in: 消费者,只能消费 T
interface Consumer<in T> {
    fun consume(item: T)
    // fun produce(): T    // 编译错误!in 参数位置不能返回 T
}

// 实际应用
interface List<out E> {    // List 是 E 的生产者
    fun get(index: Int): E    // E 在 out 位置(返回值)
    // fun set(index: Int, element: E)    // List 没有这个方法
}

interface MutableList<E> : List<E> {    // MutableList 不变
    fun add(element: E)    // E 在 in 位置
    fun get(index: Int): E    // E 在 out 位置
}

2.3.3 使用处型变

类似于 Java 的通配符,Kotlin 支持在使用处指定型变:

kotlin
// Java: void addAll(Collection<? extends E> c)
// Kotlin:
fun <T> MutableList<T>.addAll(elements: Collection<T>): Boolean

// 使用处型变
fun <T> copy(from: List<T>, to: MutableList<T>) {
    // ...
}

// 更灵活:使用 in/out 投影
fun <T> copy(from: List<out T>, to: MutableList<in T>) {
    // from 是 T 的生产者(可以读取 T)
    // to 是 T 的消费者(可以写入 T)
}

2.3.4 星投影(Star Projection)

当不知道泛型参数的具体类型时,可以使用星投影 *:

kotlin
// List<*> 等价于 List<out Any?>
val list: List<*> = listOf(1, "hello", true)

// 只能读取,不能写入(因为不知道具体类型)
val first: Any? = list[0]    // OK
// list.add(42)    // 编译错误

// 实际应用场景
fun printFirst(list: List<*>) {
    if (list.isNotEmpty()) {
        println(list[0])
    }
}

2.4 类型系统的高级特性

2.4.1 Nothing 类型

Nothing 是所有类型的子类型,表示"没有值":

kotlin
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

// Nothing 作为类型参数
val emptyList: List<Nothing> = emptyList()
// List<Nothing> 是 List<T> 的子类型(因为 List 是 out 的)

// 与 Elvis 结合
val name: String = nullableName ?: fail("Name is required")
// fail() 返回 Nothing,编译器知道 else 分支不会产生值

2.4.2 具体化类型的工厂

kotlin
inline fun <reified T : Enum<T>> enumValueOfOrNull(name: String): T? {
    return enumValues<T>().find { it.name == name }
}

enum class Color { RED, GREEN, BLUE }

val color = enumValueOfOrNull<Color>("RED")    // Color.RED
val invalid = enumValueOfOrNull<Color>("YELLOW")    // null

三、实战案例

3.1 案例一:类型安全的构建器模式

kotlin
// 使用泛型和类型约束实现类型安全的构建器
class Builder<T> {
    private val values = mutableMapOf<String, Any?>()
    
    infix fun String.to(value: Any?) {
        values[this] = value
    }
    
    inline fun <reified V> get(key: String): V? {
        return values[key] as? V
    }
    
    fun build(): Map<String, Any?> = values.toMap()
}

fun <T> build(block: Builder<T>.() -> Unit): Map<String, Any?> {
    return Builder<T>().apply(block).build()
}

fun main() {
    val config = build<Any> {
        "host" to "localhost"
        "port" to 8080
        "debug" to true
    }
    
    println(config)
}

3.2 案例二:泛型 Result 类型

kotlin
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
    object Loading : Result<Nothing>()
    
    fun <R> map(transform: (T) -> R): Result<R> = when (this) {
        is Success -> Success(transform(data))
        is Error -> this
        Loading -> this
    }
    
    fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
        is Success -> transform(data)
        is Error -> this
        Loading -> this
    }
    
    fun getOrElse(default: @UnsafeVariance T): T = when (this) {
        is Success -> data
        else -> default
    }
    
    companion object {
        inline fun <reified T> of(block: () -> T): Result<T> {
            return try {
                Success(block())
            } catch (e: Exception) {
                Error(e)
            }
        }
    }
}

fun main() {
    val result = Result.of { "42".toInt() }
    
    val mapped = result
        .map { it * 2 }
        .map { "Result: $it" }
    
    when (mapped) {
        is Result.Success -> println(mapped.data)
        is Result.Error -> println("Error: ${mapped.exception.message}")
        Result.Loading -> println("Loading...")
    }
}

3.3 案例三:使用 reified 实现类型安全的偏好存储

kotlin
import android.content.SharedPreferences

inline fun <reified T> SharedPreferences.get(key: String, default: T): T {
    return when (T::class) {
        String::class -> getString(key, default as String) as T
        Int::class -> getInt(key, default as Int) as T
        Long::class -> getLong(key, default as Long) as T
        Boolean::class -> getBoolean(key, default as Boolean) as T
        Float::class -> getFloat(key, default as Float) as T
        else -> throw IllegalArgumentException("Unsupported type: ${T::class}")
    }
}

inline fun <reified T> SharedPreferences.put(key: String, value: T) {
    edit().apply {
        when (T::class) {
            String::class -> putString(key, value as String)
            Int::class -> putInt(key, value as Int)
            Long::class -> putLong(key, value as Long)
            Boolean::class -> putBoolean(key, value as Boolean)
            Float::class -> putFloat(key, value as Float)
            else -> throw IllegalArgumentException("Unsupported type: ${T::class}")
        }
    }.apply()
}

// 使用
// val name: String = prefs.get("name", "")
// prefs.put("name", "Alice")
// val count: Int = prefs.get("count", 0)

四、本章小结

第5章深入讲解了 Kotlin 的类型系统:

空类型系统:String 和 String? 的区分是 Kotlin 安全性的基石。平台类型处理 Java 互操作中的可空性不确定性。

泛型:Kotlin 的泛型支持类型约束(where 子句)、reified 类型参数(突破类型擦除)。

型变:声明处型变(out/in)和使用处型变(星投影)提供了精确的类型安全性。

Nothing 类型:所有类型的子类型,用于表示"永不返回"的函数。


五、思考与练习

思考题

为什么 Kotlin 的 Array<String> 不是 Array<Any> 的子类型,但 Java 的 String[] 是 Object[] 的子类型?哪种设计更安全?

reified 类型参数的底层实现原理是什么?为什么它只能用于 inline 函数?

星投影 List<*> 和 List<Any?> 有什么区别?在什么场景下应该使用星投影?

平台类型的设计是一个权衡——它牺牲了编译期安全性来换取互操作性。你认为这个权衡合理吗?

编程练习

实现一个泛型函数 merge,接收两个 Map<String, T> 参数,返回合并后的 Map。

使用 reified 类型参数实现一个 deserialize<T> 函数,从 JSON 字符串反序列化为指定类型。

设计一个类型安全的事件总线,使用泛型确保事件类型和监听器类型的匹配。


5/8Kotl
0%