Skip to content

第 5 章 Null 安全 —— Kotlin 最杀手锏的特性

学习目标:理解 Kotlin 是怎么从类型系统层面消除 NPE 的;熟练使用 ?.?:!!as?letrequireNotNull 等空安全工具;知道 Java 互操作时的"平台类型"陷阱。


5.1 NPE:Java 的"十亿美元错误"

5.1.1 故事

null 这个概念是 1965 年 Tony Hoare 发明的,他后来在 2009 年的演讲中说:

"I call it my billion-dollar mistake." (我管它叫"我的十亿美元错误"。)

为什么是"十亿美元"?因为几十年来 NullPointerException 给软件行业造成的 bug 修复成本、数据丢失、宕机损失 —— 加起来远不止十亿。

5.1.2 Java 里 NPE 长啥样

java
String name = user.getProfile().getName();
System.out.println(name.length());

// 如果 user 是 null  → NPE
// 如果 profile 是 null → NPE
// 如果 name 是 null → NPE
// 编译器一句话不说,生产环境直接炸

📌 Java 的根本问题:所有引用类型都"可能是 null",但编译器无法区分。每个变量都得"心怀畏惧"地用。

5.1.3 Kotlin 的解决思路:把可空性写进类型

                   Java                    Kotlin
                   ────                     ──────
   String name = "Alice"     →     val name: String = "Alice"     // 不能为 null
                                   val name: String? = null        // 可以为 null

                                   //                  ↑
                                   //          多了一个问号
                                   //   编译器据此把代码分两类对待

5.2 可空类型:T vs T?

5.2.1 基本规则

kotlin
val name: String = "Alice"           // 非空类型,永远不能为 null
val nick: String? = null             // 可空类型,可以为 null

// 互不兼容
val s: String = nick                  // ❌ 不能把可空赋给非空
val s2: String? = name                // ✅ 可以把非空赋给可空(更宽容)

📌 类比

  • T 像「保证装着东西的快递盒」,拆了一定有内容
  • T? 像「可能是空的快递盒」,拆之前要确认

5.2.2 用可空类型时的"强制确认"

kotlin
val nick: String? = readLine()    // 用户可能直接回车 → null

// ❌ 编译报错:nick 是 String?,不能直接 .length
println(nick.length)

// 编译器告诉你:要么 ?.(安全调用),要么 !!(强制非空),要么先判断

5.3 五大空安全武器

5.3.1 ?. 安全调用(Safe Call)

kotlin
val nick: String? = null
val length = nick?.length         // null 时整个表达式返回 null,不抛异常
println(length)                   // null

链式调用特别好用:

kotlin
val city = user?.address?.city   // 任何一环为 null,整个链返回 null

📌 生活化类比?. 就像「先敲门再进去」 —— 没人应答(null)就掉头走,不硬闯(不抛异常)。

5.3.2 ?: Elvis 操作符(猫王操作符)

名字来源是「?: 旋转 90 度像猫王 Elvis 的发型」🎸

kotlin
val nick: String? = null
val display = nick ?: "(无昵称)"       // null 时用右边的默认值
println(display)                       // "(无昵称)"

// 跟 Java 三元的对比
// Java:    String d = nick == null ? "无" : nick;
// Kotlin:  val d = nick ?: "无"     ← 简洁多了

经典组合:?. + ?:

kotlin
val length = nick?.length ?: 0

Elvis + return / throw

kotlin
fun parse(input: String?): Int {
    val s = input ?: return -1                  // null 直接返回
    val n = s.toIntOrNull() ?: throw IllegalArgumentException("not int")
    return n
}

5.3.3 !! 非空断言(自杀式操作符)

kotlin
val nick: String? = null
val length = nick!!.length            // 我保证不为 null —— 错了立即抛 NPE

⚠️ !! 是 Kotlin 里"反 Kotlin 哲学"的操作符。它告诉编译器"我比你聪明,别管我"。

能不用就不用!每次写 !! 都要问自己:"如果这里真是 null,我能接受立即崩溃吗?"

5.3.4 as? 安全类型转换

kotlin
val obj: Any = "hello"
val n: Int = obj as Int               // ❌ 抛 ClassCastException
val n: Int? = obj as? Int             // ✅ 转不成返回 null
println(n)                            // null

5.3.5 智能转型(Smart Cast)

kotlin
fun foo(s: String?) {
    if (s != null) {
        println(s.length)             // ✅ 编译器知道 s 在这个分支非空
    }
}

// 也支持 Elvis 风格
fun foo(s: String?) {
    val safe = s ?: return
    println(safe.length)              // ✅ safe 是非空的 String
}

📌 Smart Cast 失效的地方:详见第 3 章的 Q9。简单说:var 和开放属性不能 smart cast,要复制到本地 val


5.4 安全调用 + Lambda:?.let { }

kotlin
val nick: String? = "Alice"

// 老 Java 风格
if (nick != null) {
    println("nick = $nick, length = ${nick.length}")
}

// Kotlin 风格
nick?.let {
    println("nick = $it, length = ${it.length}")
}

📌 ?.let { } 的妙处

  1. 块里的 it 是非空类型
  2. 不污染外层作用域
  3. 可以链式 ?.let { }?.also { }?.run { }

真实场景:链式可空判断

kotlin
fun showCity(user: User?) {
    user?.address?.city?.let { city ->
        println("城市:$city")
    } ?: println("未填写城市")
}

5.5 工具函数:requireNotNullcheckNotNull

kotlin
fun process(input: String?) {
    val name = requireNotNull(input) { "input 不能为空" }
    // 之后 name 就是非空的 String,编译器认账
    println(name.length)
}
函数用途失败时抛
requireNotNull(x)检查参数IllegalArgumentException
checkNotNull(x)检查内部状态IllegalStateException
error(msg)不可能到达的分支IllegalStateException

5.6 容器与 null

5.6.1 集合元素的可空

kotlin
val list1: List<String> = listOf("a", "b")              // 元素全部非空
val list2: List<String?> = listOf("a", null, "b")       // 元素可空
val list3: List<String>? = null                          // 列表本身可空
val list4: List<String?>? = null                         // 全可空

5.6.2 过滤掉 null

kotlin
val mixed: List<String?> = listOf("a", null, "b", null, "c")
val noNull: List<String> = mixed.filterNotNull()
println(noNull)                                          // [a, b, c]

5.7 Java 互操作:平台类型陷阱

5.7.1 平台类型是什么

Java 不区分可空/非空(除非加 @Nullable / @NotNull 注解),所以 Kotlin 看到 Java 类型时无法决定它是 T 还是 T?只好不强制检查

kotlin
// 假设 Java 里:public String getName() { ... }

val name = javaObj.getName()          // 类型是 String! (平台类型)
println(name.length)                   // 不报错 —— 但运行时如果 getName 返回 null,仍 NPE

String! 是"既能当 String 用、又能当 String? 用"的尴尬类型。平台类型只在 Kotlin 推断时存在,你不能显式声明 String!

5.7.2 防御性写法

kotlin
// 调 Java API 时,明确声明你认为它是不是可空
val name: String = javaObj.getName()      // 你认为它非空 —— 错的话立即 NPE,但比"埋雷晚炸"好
val name: String? = javaObj.getName()     // 你认为它可空 —— 强迫自己处理 null

5.7.3 Kotlin 该如何告诉 Java "我返回了非空"

kotlin
fun greet(name: String): String = "Hello, $name"

Kotlin 编译后会给 Java 看到 @NotNull String 注解:

java
// Java 调用方收到的字节码签名
public static @NotNull String greet(@NotNull String name) { ... }

// Java 端如果传 null 给 name,会立即抛
//   java.lang.IllegalArgumentException: Parameter specified as non-null is null

5.8 实战:解析 JSON 数据

假设我们从 API 拿到一个 user JSON:

json
{
  "name": "Alice",
  "address": {
    "city": "Beijing",
    "zipcode": null
  }
}
kotlin
data class Address(val city: String?, val zipcode: String?)
data class User(val name: String, val address: Address?)

fun displayUserInfo(user: User?) {
    val display = buildString {
        append("姓名:${user?.name ?: "(未知)"}\n")
        append("城市:${user?.address?.city ?: "(未填写)"}\n")
        append("邮编:${user?.address?.zipcode ?: "(未填写)"}\n")
    }
    println(display)
}

fun main() {
    val u1 = User("Alice", Address("Beijing", null))
    val u2 = User("Bob", null)
    val u3: User? = null

    displayUserInfo(u1)
    displayUserInfo(u2)
    displayUserInfo(u3)
}

输出:

姓名:Alice
城市:Beijing
邮编:(未填写)

姓名:Bob
城市:(未填写)
邮编:(未填写)

姓名:(未知)
城市:(未填写)
邮编:(未填写)

注意:用 Kotlin 写出来这段代码,根本不可能抛 NPE。这就是空安全的威力。


5.9 章末小结

                ★ 第 5 章核心知识图谱 ★

        ┌─────────────────┼─────────────────┐
        │                 │                 │
     ┌──▼──┐          ┌──▼───┐         ┌──▼───┐
     │ 类型 │          │ 操作符│         │ 工具  │
     ├─────┤          ├──────┤         ├──────┤
     │T 非空│          │ ?.    │         │let    │
     │T? 可空│         │ ?:    │         │require│
     │T! 平台│         │ !!    │         │filter │
     │      │          │ as?   │         │NotNull│
     └─────┘          └──────┘         └──────┘

记忆口诀

  • ?. —— 小心走(null 就掉头)
  • ?: —— 给后路(null 就用默认)
  • !! —— 赌一把(null 就崩)
  • as? —— 试试看(不行返回 null)
  • let —— 进屋办事(块内非空)

🎤 5.10 章末面试题(10 道)

Q1. Kotlin 是怎么避免 NPE 的?

:核心是把"是否可空"编进类型系统

  • String 表示非空,编译器禁止赋 null
  • String? 表示可空,访问时编译器强制处理 null 的情况

NPE 这个"运行时炸弹"被提前到编译期发现,从源头消除。


Q2. ?.?:!! 三个操作符各是什么?

操作符名字行为
?.安全调用左边为 null 时整个表达式返回 null,不抛异常
?:Elvis左边为 null 时返回右边的值,否则返回左边
!!非空断言强制当成非空,为 null 时立即抛 NPE

经典组合:val len = name?.length ?: 0(name 为 null 则 len 为 0)


Q3. !! 危险吗?什么时候才该用?

99% 的场景都不该用!! 等于把 null 检查关掉,跟 Java 一样裸奔。

合理使用场景:

  1. Java 互操作时确信非空但类型推断不出来
  2. 测试代码里,故意让 null 立即崩出栈跟踪
  3. 本地变量临时绕过编译器(很少需要)

💡 在 IDE 里把"!! 警告"开成 ERROR 级别,能强制团队重视。


Q4. 什么是"平台类型"(Platform Type)?

:Kotlin 调 Java 代码时,因为 Java 不区分可空/非空(除非加注解),Kotlin 不知道这个 Java 返回值是不是会 null,只好把类型标为 String!

String! 既能当 String 也能当 String? 用 —— 灵活但不安全。

实际写代码时不能直接用 String!,应该明确声明:

kotlin
val name: String = javaApi.getName()      // 我赌它非空(错就 NPE)
val name: String? = javaApi.getName()     // 我假设它可空(更安全)

Q5. asas? 有啥区别?

  • as:强制转换,转不成抛 ClassCastException
  • as?:安全转换,转不成返回 null
kotlin
val obj: Any = "hello"
val n1: Int = obj as Int             // ❌ ClassCastException
val n2: Int? = obj as? Int           // ✅ null

实战中 as? 配合 ?: 用得最多:

kotlin
val n: Int = (obj as? Int) ?: 0      // 转不成就用 0

Q6. 为啥写 if (s != null) { s.length } 编译能过,但 if (obj.s != null) { obj.s.length } 不能?

:Smart Cast 的限制。

s 是局部 val,编译器能保证 "判完 != null 到使用之间没人改 s" → 可以 smart cast。

obj.s 是开放属性(或 var),编译器无法保证两次访问之间没有别的线程或代码改动它,所以拒绝 smart cast。

解决办法:复制到本地 val

kotlin
val s = obj.s
if (s != null) { println(s.length) }     // ✅

Q7. ?.let { }if (x != null) { } 有什么区别?

维度if (x != null) { }x?.let { }
是否表达式否(语句)(有返回值)
块内变量名还是 xit(或自定义)
链式调用不优雅完美:a?.b?.let { ... }?.also { ... }
智能转型依赖 smart cast,对 var/属性失效保证非空
写法风格命令式函数式

经验法则:单层判断用 if,多层链式用 ?.let { }


Q8. Kotlin 里有"双问号变量" String?? 吗?

没有? 不是符号叠加。String? 已经是"String 或 null"的完整含义,再加问号没意义。

但泛型场景里你会看到 T? 嵌套:

kotlin
fun <T> List<T>.firstOrNullSafe(): T? = firstOrNull()
// 如果 T 本身是 String?,那 T? 实际上还是 String?

Q9. 集合里的元素可以是 null 吗?

:可以,看类型声明:

kotlin
val a: List<String>     // 元素全部非空
val b: List<String?>    // 元素可以是 null
val c: List<String>?    // 列表本身可以是 null
val d: List<String?>?   // 双重可空

实用方法:

kotlin
listOf("a", null, "b").filterNotNull()         // 过滤掉 null → List<String>
mapOf("k" to null).getOrElse("k") { "default" } // 取不到给默认值

Q10. 为啥 Kotlin 标准库里不用 Optional?

:因为 T? 已经够用,而且更轻

Java 8 引入 Optional<T> 也是为了显式表达可空性,但:

  • 每用一次都要装箱(额外对象)
  • 调用要 opt.map().orElse(),啰嗦
  • 不能强制让旧 API 全用 Optional

Kotlin 用类型系统解决,零运行时开销String? 在 JVM 字节码里就是 String,"可空性"信息只存在于编译期。


下一章 → 第 6 章 · 类与对象 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

kotlin
/**
 * 第 5 章 · Null 安全 — 综合示例
 *
 * 包含:
 *   - 可空类型与非空类型
 *   - ?. 安全调用
 *   - ?: Elvis 操作符
 *   - !! 非空断言(演示其危险性)
 *   - as? 安全转换
 *   - ?.let { } 链式
 *   - 集合的可空处理
 *   - 平台类型陷阱
 */

data class Address(val city: String?, val zipcode: String?)
data class User(val name: String, val nickname: String?, val address: Address?)

fun main() {
    println("=== 1. 可空 vs 非空 ===")
    val name: String = "Alice"
    val nick: String? = null
    println("name = $name (非空)")
    println("nick = $nick (可空)")
    // val s: String = nick   // ❌ 编译报错:Type mismatch

    println("\n=== 2. ?. 安全调用 ===")
    println("nick?.length = ${nick?.length}")             // null
    println("name?.length = ${name?.length}")             // 5

    println("\n=== 3. ?: Elvis 默认值 ===")
    val displayNick = nick ?: "(无昵称)"
    println("displayNick = $displayNick")
    val len = nick?.length ?: 0
    println("len = $len")

    println("\n=== 4. !! 危险演示 ===")
    val safeName: String? = "Bob"
    println("safeName!!.length = ${safeName!!.length}")   // 5

    try {
        val nullName: String? = null
        println("nullName!!.length = ${nullName!!.length}")
    } catch (e: NullPointerException) {
        println("⚠️ 捕获到 NPE: ${e.message ?: "no message"}")
        println("   → !! 是自杀式操作符,能不用就别用!")
    }

    println("\n=== 5. as? 安全转换 ===")
    val obj: Any = "hello"
    val asInt: Int? = obj as? Int           // null
    val asString: String? = obj as? String  // "hello"
    println("obj as? Int = $asInt")
    println("obj as? String = $asString")
    val n: Int = (obj as? Int) ?: -1
    println("用默认值: $n")

    println("\n=== 6. Smart Cast ===")
    fun describe(x: Any?) = when {
        x == null -> "null"
        x is String -> "字符串,长度 ${x.length}"   // 自动当 String 用
        x is Int -> "整数,平方 ${x * x}"
        else -> "其他类型 ${x::class.simpleName}"
    }
    listOf(null, "hello", 5, 3.14).forEach {
        println("${it} → ${describe(it)}")
    }

    println("\n=== 7. ?.let { } 链式 ===")
    val users = listOf(
        User("Alice", "alice123", Address("Beijing", "100000")),
        User("Bob", null, Address("Shanghai", null)),
        User("Charlie", "charlie", null),
    )

    users.forEach { user ->
        // 老 Java 风格
        if (user.address != null && user.address.city != null) {
            // ↑ 这里 smart cast 不一定成功(开放属性问题),实际可能要本地变量
            // println("城市: ${user.address.city}")
        }
        // Kotlin 风格
        user.address?.city?.let { city ->
            println("${user.name} 住在 $city")
        } ?: println("${user.name} 没填城市")
    }

    println("\n=== 8. 集合的 null 处理 ===")
    val mixed: List<String?> = listOf("a", null, "b", null, "c")
    println("原始: $mixed")
    println("过滤掉 null: ${mixed.filterNotNull()}")
    println("用空字符串代替 null: ${mixed.map { it ?: "" }}")
    println("非 null 元素长度之和: ${mixed.sumOf { it?.length ?: 0 }}")

    println("\n=== 9. requireNotNull / checkNotNull ===")
    fun process(input: String?) {
        val name = requireNotNull(input) { "input 不能为空" }
        println("处理: $name (长度 ${name.length})")
    }
    process("hello")
    try {
        process(null)
    } catch (e: IllegalArgumentException) {
        println("⚠️ 捕获: ${e.message}")
    }

    println("\n=== 10. 实战:解析嵌套结构 ===")
    val u = User("David", null, Address(null, null))
    val info = buildString {
        append("姓名:${u.name}\n")
        append("昵称:${u.nickname ?: "(未设置)"}\n")
        append("城市:${u.address?.city ?: "(未填写)"}\n")
        append("邮编:${u.address?.zipcode ?: "(未填写)"}")
    }
    println(info)
}

NullSafetyDemo.kt ↗