Kotlin 空安全实战:用类型系统告别 NullPointerException 的崩溃噩梦
Tony Hoare(快速排序的发明者)曾在 2009 年公开道歉,说他发明 null 引用是一个"十亿美元的错误"。在 Java 世界里,NullPointerException(NPE)几乎是每个程序员的日常噩梦——你以为对象不为空,结果运行时直接崩溃,堆栈信息还告诉你崩溃发生在第 3 层调用之后。
// Java:经典的 NPE 陷阱
String city = user.getAddress().getCity().getName();
// 如果 user 为 null?NPE!
// 如果 getAddress() 返回 null?NPE!
// 如果 getCity() 返回 null?NPE!
为了防 NPE,你不得不写一堆 if (xxx != null) 的防御性代码。而 Kotlin 的解决方案是:在类型系统层面就区分"可以为空"和"不能为空",让编译器帮你把关,把 NPE 从运行时提前到编译时。
第一步:可空类型 vs 非空类型
在 Kotlin 中,默认所有变量都是非空的。如果你想让一个变量可以为 null,必须在类型后面加一个 ?:
var name: String = "Alice"
name = null // 编译错误!String 类型不允许为 null
var nickname: String? = "Bob"
nickname = null // 没问题,String? 允许为 null
就这么简单的一行,编译器就帮你挡住了第一道防线:如果一个变量的类型没有 ?,它永远不可能是 null。你不需要在运行时检查,编译器在写代码的时候就告诉你了。
安全调用 ?.:null 时不崩溃
当你有一个可空类型的变量,直接调用它的方法会编译报错:
val email: String? = null
println(email.length) // 编译错误!email 可能为 null
Kotlin 提供了 安全调用操作符 ?.——如果对象为 null,整个表达式返回 null,不会崩溃:
val email: String? = null
println(email?.length) // 输出 null,不崩溃
val email2: String? = "alice@example.com"
println(email2?.length) // 输出 21
?. 可以链式调用,这是它最强大的地方:
data class Address(val city: String)
data class User(val name: String, val address: Address?)
fun getCity(user: User?): String? {
return user?.address?.city
// 如果 user 为 null → 返回 null
// 如果 address 为 null → 返回 null
// 如果都不为 null → 返回 city
}
换成 Java,你需要写三层 if (xxx != null) 嵌套。Kotlin 一行搞定。
Elvis 运算符 ?::给 null 一个默认值
安全调用返回的可能是 null,但很多时候你需要一个确定的非空值。这时候 Elvis 运算符 ?: 就派上用场了——它读起来像一个倒过来的猫王发型:
val email: String? = null
// 如果左边为 null,取右边的值
val display = email ?: "未设置邮箱"
println(display) // 输出:未设置邮箱
val email2: String? = "alice@example.com"
val display2 = email2 ?: "未设置邮箱"
println(display2) // 输出:alice@example.com
?: 经常和 ?. 搭配使用:
val user: User? = null
val city = user?.address?.city ?: "未知城市"
println(city) // 输出:未知城市
更妙的是,Elvis 右边还可以直接写 return 或 throw:
fun processEmail(email: String?) {
val safeEmail = email ?: return // email 为 null 时直接退出函数
println("发送邮件到 $safeEmail")
}
fun parsePort(text: String?): Int {
return text?.toIntOrNull() ?: throw IllegalArgumentException("端口无效: $text")
}
非空断言 !!:最后的手段
如果你 100% 确定一个可空变量此刻不为 null,可以用 !! 强制解包:
val email: String? = "alice@example.com"
println(email!!.length) // 输出 21
但如果 !! 的变量实际上是 null,就会抛出 KotlinNullPointerException:
val email: String? = null
println(email!!.length) // 运行时崩溃!KotlinNullPointerException
最佳实践:在业务代码中尽量消灭 !!。 它是 Kotlin 中唯一主动制造 NPE 的操作符。如果你发现自己想写 !!,先想想能不能用 ?. 或 ?: 替代。
| 写法 | 含义 | 推荐程度 |
|---|---|---|
x?.foo() | x 为 null 时返回 null | 推荐 |
x?.foo() ?: default | x 为 null 时用默认值 | 推荐 |
x!!.foo() | 强制认为 x 不为 null,否则崩溃 | 尽量避免 |
智能转换:if 判空后自动收窄类型
Kotlin 编译器很聪明——当你在 if 里检查了 null,后续代码会自动把类型"收窄"为非空:
fun greet(name: String?) {
if (name != null) {
// 在这个分支里,name 的类型自动从 String? 变成 String
println("Hello, $name! 名字长度: ${name.length}")
// 不需要 name?.length,直接 name.length 就行
} else {
println("Hello, stranger!")
}
}
这就是 智能转换(Smart Cast)。编译器帮你追踪了 null 检查的结果,省去了手动转换的麻烦。
let:null 安全地执行代码块
?.let { } 是一个非常实用的组合——只有当对象不为 null 时,才执行 let 里的代码块,并且 it 自动是非空类型:
val email: String? = "alice@example.com"
email?.let {
// 只有 email 不为 null 时才会执行
println("发送邮件到 $it")
println("邮件长度: ${it.length}")
// it 的类型是 String(非空),不需要 ?.
}
// 等价于:
if (email != null) {
println("发送邮件到 $email")
println("邮件长度: ${email.length}")
}
?.let 在处理集合时特别好用:
val users: List<User>? = getUsers() // 可能返回 null
users?.let { list ->
// 只有 users 不为 null 时才遍历
list.forEach { println(it.name) }
}
实战:解析用户输入
把今天学的全部组合起来,写一个真实的场景——解析用户表单输入:
data class UserProfile(
val name: String?,
val email: String?,
val age: String?,
val bio: String?
)
fun validateProfile(profile: UserProfile): String {
// 姓名:必填,为空则报错
val name = profile.name?.trim()
?: return "姓名不能为空"
// 邮箱:必填,且需要包含 @
val email = profile.email?.trim()
?: return "邮箱不能为空"
if (!email.contains("@")) return "邮箱格式不正确"
// 年龄:选填,为空时默认 18
val age = profile.age?.toIntOrNull() ?: 18
// 简介:选填,为空时显示"这个人很懒,什么都没写"
val bio = profile.bio?.takeIf { it.isNotBlank() }
?: "这个人很懒,什么都没写"
return "验证通过!$name ($email), $age 岁, 简介: $bio"
}
fun main() {
val profile = UserProfile(
name = " Alice ",
email = "alice@example.com",
age = null,
bio = null
)
println(validateProfile(profile))
// 输出:验证通过!Alice (alice@example.com), 18 岁, 简介: 这个人很懒,什么都没写
}
这段代码用到了今天学的几乎所有技巧:
?.trim():安全调用?: return:Elvis + 提前返回?.toIntOrNull() ?: 18:安全调用 + Elvis 默认值?.takeIf { } ?::条件过滤 + 兜底
小结
| 操作符 | 用途 | 记忆技巧 |
|---|---|---|
Type? | 声明可空类型 | "打个问号,可能为空" |
?. | 安全调用 | "为空就跳过" |
?: | Elvis 默认值 | "为空就取右边" |
!! | 非空断言 | "我赌它不为空"(慎用) |
?.let { } | null 安全代码块 | "不为空才执行" |
if (x != null) | 智能转换 | "判空后自动收窄类型" |
一句话总结:Kotlin 把 null 检查从"运行时祈祷"变成了"编译时保证"。 只要你的变量类型没有 ?,它就不可能是 null——这是 Kotlin 送给每一个被 NPE 折磨过的程序员最好的礼物。
今日小挑战:下面这段代码的输出是什么?为什么?试着在脑子里跑一遍,然后用 Kotlin Playground 验证你的答案。
fun main() {
val a: String? = null
val b: String? = "Hello"
println(a ?: "default")
println(b?.length)
println(b?.length ?: 0)
println(a?.length ?: b?.length ?: -1)
}
提示:?: 可以链式使用——如果左边为 null,就取右边;右边又是一个 ?: 表达式。