类型系统与空安全

Kotlin 的类型系统是它区别于 Java 的核心特性之一。你可以把 Kotlin 的类型理解为“编译器参与设计 API 契约”的工具:哪些值可以为空、哪些值不能为空、什么时候需要显式处理异常路径,都通过类型表达出来。

一切都像对象

在 Kotlin 中,你可以对数字、布尔值、字符等基础类型调用成员函数:

val number = 42
println(number.toString())
println(number.inc())

这不代表 Kotlin 在运行时一定把所有值都装箱成对象。对于 JVM 后端,数字、布尔值等类型在很多情况下会使用高效的原始类型表示。你在源码层面看到的是统一的对象模型,编译器会负责优化运行时表示。

Java 对比:

  • Java 有 intInteger 的区别。
  • Kotlin 源码中通常只写 Int
  • 当泛型、可空类型或 Java 互操作需要时,Kotlin 可能发生装箱。

常见基础类型

Kotlin Java 接近类型 说明
Byte byte / Byte 8 位整数
Short short / Short 16 位整数
Int int / Integer 32 位整数
Long long / Long 64 位整数
Float float / Float 32 位浮点数
Double double / Double 64 位浮点数
Boolean boolean / Boolean 布尔值
Char char / Character 字符
String String 字符串
Array<T> T[] 数组

非空类型与可空类型

Kotlin 普通类型默认非空:

val name: String = "Ada"
println(name.length)

下面的代码不能编译:

// val name: String = null

如果变量确实可能为空,类型后面加 ?

val name: String? = null

此时不能直接访问成员:

// println(name.length) // 编译错误

编译器要求你先处理空值。这就是 Kotlin 空安全的核心:把 Java 中经常运行时才暴露的 NullPointerException,尽量提前到编译期暴露。

显式空值判断与智能转换

fun printLength(text: String?) {
    if (text != null) {
        println(text.length)
    }
}

if (text != null) 成立的分支里,编译器知道 text 不再是 String?,而是 String。这叫智能转换。

智能转换依赖编译器能确认变量在检查后没有被改变。局部 val 最容易被智能转换;可变属性、开放属性或跨线程可变值通常不一定能被安全转换。

安全调用:?.

val name: String? = null
println(name?.length) // null

name?.length 的含义是:

  • 如果 name 不为空,返回 name.length
  • 如果 name 为空,整个表达式返回 null

类型也会变化:String.lengthInt,但 name?.lengthInt?

安全调用可以链式使用:

val cityName = user?.address?.city?.name

只要链条中任何一步是 null,结果就是 null

Elvis 运算符:?:

?: 用于为空时给默认值:

fun displayName(name: String?): String {
    return name ?: "匿名用户"
}

常见组合:

val length = name?.length ?: 0

因为 returnthrow 在 Kotlin 中也是表达式,所以可以写:

fun requireName(name: String?): String {
    return name ?: throw IllegalArgumentException("name 不能为空")
}

或提前返回:

fun sendEmail(email: String?) {
    val validEmail = email ?: return
    println("发送邮件到 $validEmail")
}

非空断言:!!

val name: String? = getName()
println(name!!.length)

!! 的意思是:“我确信它不是 null,如果错了就抛异常。”这会把 Kotlin 的空安全优势交还给运行时。

建议:

  • 业务代码中尽量少用 !!
  • 如果能用 ?: return?: throw?.let {},优先使用这些表达更明确的写法。
  • !! 更适合测试代码、临时迁移代码或你确实能证明不为空但编译器无法推断的边界。

let 处理非空值

val email: String? = findEmail()

email?.let {
    println("发送邮件到 $it")
}

?.let {} 表示只有左侧不为空时才执行代码块。在代码块中,it 是非空类型。

如果逻辑很简单,let 很方便;如果代码块很长,显式 if (email != null) 可能更清晰。

安全类型转换:as?

普通转换失败会抛异常:

val value: Any = "Kotlin"
val text = value as String

安全转换失败返回 null

fun lengthOfString(value: Any): Int {
    val text = value as? String
    return text?.length ?: -1
}

Java 对比:

int lengthOfString(Object value) {
    if (value instanceof String text) {
        return text.length();
    }
    return -1;
}

可空集合元素

集合本身可空和元素可空是两回事:

val a: List<String>? = null         // 列表引用可能为空
val b: List<String?> = listOf(null) // 列表不为空,但元素可能为空
val c: List<String?>? = null        // 两者都可能为空

过滤空元素:

val names: List<String?> = listOf("Ada", null, "Grace")
val nonNullNames: List<String> = names.filterNotNull()

Kotlin 中仍可能出现 NPE 的情况

Kotlin 大幅减少空指针,但不是绝对杜绝。常见来源:

  • 显式 throw NullPointerException()
  • 使用 !!
  • 构造或初始化顺序问题,例如在对象未初始化完成时泄露 this
  • Java 互操作,尤其是没有空性注解的 Java API。
  • Java 代码向 Kotlin 期望非空的泛型集合中放入 null

实践建议

  • 公共 API 的可空性要认真设计,不要随手写 String?
  • 能用非空类型就不要用可空类型。
  • 可空值进入函数后尽早处理,例如提前返回、抛出明确异常或转成默认值。
  • 不要把 !! 当作 IDE 提示的“快速修复”。
  • 与 Java API 交互时,尽量补充 @Nullable / @NotNull / JSpecify 注解。

参考

  • 官方类型概览:https://kotlinlang.org/docs/types-overview.html
  • 官方空安全:https://kotlinlang.org/docs/null-safety.html
  • Java 与 Kotlin 空性对比:https://kotlinlang.org/docs/java-to-kotlin-nullability-guide.html