Kotlin 空安全(Null Safety)

这是 Kotlin 区别于 Java 的最大改进,也是它最常被称赞的特性。Tony Hoare 在 1965 年发明了 null 引用,后来他称之为"十亿美元错误"——因为 null 导致的 NullPointerException 是 Java 程序员挥之不去的噩梦。Kotlin 在类型系统层面解决了这个问题,让 NPE 几乎不可能发生。

1. 核心思想:类型系统区分可空与非空

Kotlin 给每个引用类型加了两个版本:非空(String)和可空(String?)。非空类型永远不能为 null,可空类型才可以:

// 核心思想:类型系统区分【可空】和【非空】
var a: String = "abc"
a = "def"               // OK
// a = null             // 编译错误!非空类型不能赋 null

var b: String? = "abc"  // 注意 String?,问号表示可空
b = null                // OK
b = "xyz"               // 也 OK

// 这从【编译期】杜绝了 NullPointerException
// 不像 Java:任何 String 都可能是 null,你不查就崩

这一个看似简单的改动,从编译期消灭了大多数 NPE。Java 里任何 String 都可能是 null,你必须时刻警惕、到处 if (x != null);Kotlin 里只要类型是 String,你就放心用,编译器保证它不为 null。

2. 安全调用 ?.(招牌运算符)

对可空类型,直接访问成员会编译错误。必须用 ?. 安全调用:

// 非空类型:直接访问成员
val a: String = "hello"
println(a.length)        // 5,放心调用

// 可空类型:直接访问【会编译错误】
val b: String? = null
// println(b.length)    // 编译错误!b 可能为 null

// 必须用安全调用 ?. 或非空断言 !!
println(b?.length)       // null(b 为 null 时整个表达式为 null)

// 安全调用链
class User(val profile: Profile?)
class Profile(val email: String?)

val user: User? = User(Profile(null))
val email = user?.profile?.email      // String?(可能为 null)
println(email)                        // null

?. 的语义:如果接收者是 null,整个表达式就是 null;如果不是 null,正常调用。可以链式使用:user?.profile?.email,任一环为 null 整个就 null,避免了 Java 的 if (user != null && user.profile != null && ...) 啰嗦链。

3. Elvis 运算符 ?:(给 null 一个默认)

名字来自摇滚歌手 Elvis Presley(他的发型像 ?: 符号)。语义:为 null 时给默认值:

// ?: Elvis 运算符:为 null 时给默认值
val name: String? = null
val display: String = name ?: "匿名"
println(display)                      // 匿名

// 链式用法
val len: Int = name?.length ?: 0
println(len)                          // 0

// 配合 return / throw(常用模式)
fun greet(name: String?) {
    val n = name ?: return             // 为 null 就直接 return
    println("你好, $n")
}

fun findUser(id: Int): User {
    return db.find(id) ?: throw NotFoundException("用户 $id 不存在")
}

// ?: 右边是"任意表达式",所以可以写很灵活的逻辑
val value = nullable ?: computeDefault()

Elvis 配合 return/throw 是处理 null 的惯用模式:val n = name ?: return(为 null 就早返回),val user = findUser() ?: throw NotFound()(找不到就抛异常)。代码紧凑且读起来像自然语言。

4. 非空断言 !!(慎用!)

!! 强制把可空类型当非空用——如果为 null,抛 NPE:

// !! 非空断言:为 null 就抛 NullPointerException
val name: String? = "小明"
val len1: Int = name!!.length          // 编译通过(运行时可能崩)

val bad: String? = null
// val len2 = bad!!.length             // 运行时抛 NullPointerException

// 何时用 !!?
// 1. 你【非常确定】这里不为 null(例如刚检查过、构造函数初始化过)
// 2. 测试代码、本地脚本(快速试错)
// 3. 与 Java 代码互操作,确信对方不会返回 null

// 经验法则:【能用 ?. 就别用 !!】
// !! 等于关闭了 Kotlin 的空安全,是"逃避"的做法
// IDE 会高亮 !!,提示这里是危险的

何时用 !!?原则是尽量不用,能用 ?.?: 替代就替代。!! 等于关闭了 Kotlin 的空安全,是"逃避"做法。IDE 会用黄色高亮 !! 提醒你这行有风险。

合理使用 !! 的场景:

5. ?.let { } 模式(只在非空时执行)

对可空变量做一系列操作,用 ?.let { ... } 包起来,it 自动是非空类型:

// ?.let { } 模式:只在非空时执行某段逻辑
val name: String? = "小明"
name?.let {
    println("名字是 $it")               // it 是非空的 name
    println("长度 ${it.length}")
}

// 如果 name 为 null,let 块整个跳过
val bad: String? = null
bad?.let {
    println("这行不会执行")
}
println("继续")

// 对比 Java:
// if (name != null) {
//     System.out.println("名字是 " + name);
// }
// Kotlin 的 ?.let 更紧凑,且 it 自动是非空类型

// 适合:对可空变量做一系列操作
user?.let {
    saveToDb(it)
    sendEmail(it.email)
    logAction(it.id)
}

对比 Java 的 if (x != null) { ... },Kotlin 的 x?.let { ... } 更紧凑,且 it 类型是非空的,块内任何调用都安全。适合"对可空变量做一系列副作用"的场景。

6. 智能转换(Smart Cast)

编译器跟踪 null 检查,自动把可空类型当非空用:

// 智能转换:判断非空后,编译器自动把可空类型当非空用
val name: String? = "小明"
if (name != null) {
    // 在这个块里,name 自动是【非空 String】
    println(name.length)              // 不需要 ?. 或 !!
}

// when 也是
when (name) {
    null -> println("匿名")
    else -> println(name.length)       // 自动非空
}

// 但【注意】:可变 var 在多线程下可能被改
// 这种情况编译器会拒绝智能转换
class Foo {
    var name: String? = null

    fun check() {
        if (name != null) {
            // println(name.length)  // 编译警告:name 可能被其他线程改
            val n = name!!            // 需要显式 !! 或局部快照
        }
    }
}

智能转换是 Kotlin 的"省心"特性——你不需要重复检查 null,也不需要显式强转。但有陷阱:可变 var 属性在多线程下可能被其他线程修改,所以编译器对 var name: String? 不会做跨方法智能转换。这种情况下要么先存到局部 val,要么用 !!

7. 平台类型(Platform Type,与 Java 互操作)

当 Kotlin 调用 Java 代码时,Java 的引用类型没有可空性信息,Kotlin 把它当作平台类型(记作 String!),意为"可能是 null 也可能不是 null":

// 平台类型(Platform Type):来自 Java 的可空性未知
// 例如 Java 方法:
// public String getName() { return ...; }
//
// 在 Kotlin 里调用,返回类型是 String!(感叹号)
// 表示【可能为 null 也可能不为 null】,Kotlin 不强制检查

val fromJava: String = javaObj.getName()    // 不报错(乐观假设)
val fromJavaSafe: String? = javaObj.getName()  // 也可以当可空

// 但如果 Java 方法真的返回 null,而 Kotlin 当非空用,会崩!
// 解决:Java 调用结果一律加 @Nullable 注解或当 String? 处理

// 实战建议:
// - 第三方 Java 库的返回值:默认当可空 String? 处理
// - 加 @Nullable / @NotNull 注解,JSR-305 包,Kotlin 会识别
// - 用 kotlin.Nullable(显式断言可空)

实战建议:

8. 安全转换 as?

类型转换用 as 失败会抛 ClassCastException,用 as? 失败返回 null:

// 安全转换 as?:失败返回 null 而不是抛异常
val obj: Any = "hello"
val s: String? = obj as? String        // "hello"
val n: Int? = obj as? Int              // null(不抛 ClassCastException)

// 对比 Java:
// String s = (String) obj;  // 类型不匹配抛 ClassCastException
// Kotlin 的 as? 更安全

// 配合 Elvis 给默认值
val len = (obj as? String)?.length ?: 0
println(len)                            // 5 或 0

// 配合 when 做类型分发
fun describe(x: Any) = when (x) {
    is Int -> "整数"                  // is 触发智能转换
    is String -> "字符串"
    else -> "其他"
}

这是和 ?. 一脉相承的安全思路——不要抛异常,用 null 表示失败,然后由调用者用 Elvis 处理。

9. 集合的可空性

集合类型有三个层面的可空,容易混淆:

// 集合空安全:List<String?> vs List<String>? vs List<String?>?
// 三个完全不同的概念!

// List<String>          :列表非空,元素非空
val a: List<String> = listOf("a", "b")

// List<String?>         :列表非空,元素可能为 null
val b: List<String?> = listOf("a", null, "b")

// List<String>?         :列表可能为 null,元素非空
val c: List<String>? = null
val c2: List<String>? = listOf("a", "b")

// List<String?>?        :列表可能为 null,元素也可能为 null
val d: List<String?>? = null
val d2: List<String?>? = listOf("a", null)

// 过滤掉 null 元素
val mixed: List<String?> = listOf("a", null, "b", null, "c")
val nonNull: List<String> = mixed.filterNotNull()
println(nonNull)                      // [a, b, c]

filterNotNull()List<String?> 转成 List<String>,剔除所有 null 元素。

10. 实战:HTTP 请求解析

data class User(val name: String?, val age: Int?, val email: String?)

fun processUser(user: User?): String {
    // ?: 早返回处理 null
    val u = user ?: return "无用户"

    // 智能转换:u 已经是非空 User
    val name = u.name ?: "匿名"
    val age = u.age ?: 0
    val email = u.email?.lowercase()  // 可空链式

    return "用户 $name,年龄 $age,邮箱 ${email ?: "未填"}"
}

println(processUser(null))
// 无用户
println(processUser(User(null, null, null)))
// 用户 匿名,年龄 0,邮箱 未填
println(processUser(User("小明", 25, "XIAO@Email.com")))
// 用户 小明,年龄 25,邮箱 xiao@email.com

这个例子综合用了 Elvis、智能转换、可空调用、链式调用。注意 val u = user ?: return 后,编译器知道 u 是非空 User,直接访问 u.name 等字段。

11. 常见反模式(避免!)

小结

这一章你掌握了 Kotlin 最有价值的特性——空安全。String vs String? 是核心区别;?. 安全调用、?: Elvis、!! 非空断言、?.let 是四个主力工具。坚持用这些,你的 Kotlin 代码几乎不会再出 NPE。下一篇看字符串——Kotlin 字符串的更多操作。

← 上一篇 Kotlin 函数

下一篇 Kotlin 字符串

✈️💬