跳到主要内容

Kotlin 空安全实战:用类型系统告别 NullPointerException 的崩溃噩梦

· 阅读需 7 分钟
重庆 Kotlin User Group

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() ?: defaultx 为 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,就取右边;右边又是一个 ?: 表达式。