Kotlin 空安全:? 与 !! 操作符
Kotlin 的核心设计目标之一是消除空指针异常(NullPointerException),这在 Java 中被称为“十亿美元的错误”。为此,Kotlin 引入了空安全类型系统,通过编译器强制区分“可为空”和“不可为空”的类型。? 和 !! 是两个关键操作符,用于处理可能为 null 的值。正确使用它们,能让你的代码既安全又简洁。
理解可空类型与非空类型
在 Kotlin 中,类型默认是非空的。例如:
val name: String = "Alice"
这里的 name 不能被赋值为 null。如果你尝试写 name = null,编译器会直接报错。
要允许一个变量为 null,必须显式声明为可空类型,方法是在类型后面加 ?:
val name: String? = null
现在 name 可以是字符串,也可以是 null。
使用 ? 操作符:安全调用
当你有一个可空类型的变量时,不能直接调用它的方法或属性,因为如果它是 null,就会抛出异常。Kotlin 提供了安全调用操作符 ?. 来解决这个问题。
使用 ?. 访问属性或方法时,如果接收者为 null,整个表达式返回 null,而不是抛出异常。
例如:
val length: Int? = name?.length
- 如果
name是"Alice",length就是5。 - 如果
name是null,length就是null。
你还可以链式使用安全调用:
val city: String? = user?.address?.city
只要 user、user.address 或 city 中任意一个是 null,整个表达式就返回 null,不会崩溃。
使用 ?: 操作符:Elvis 操作符
安全调用返回的是可空类型,但很多时候你需要一个非空值。这时可以配合 Elvis 操作符 ?:,提供一个默认值。
如果左侧表达式不为 null,返回它;否则返回右侧的值。
val displayName: String = name ?: "Guest"
- 如果
name是"Alice",displayName就是"Alice"。 - 如果
name是null,displayName就是"Guest"。
你甚至可以把安全调用和 Elvis 操作符连用:
val len: Int = name?.length ?: 0
这样 len 一定是 Int 类型,不会是 Int?。
使用 !! 操作符:强制断言非空
有时候你确定一个可空变量实际上不是 null(比如经过了 if 判断,或者来自外部系统的保证),但编译器不知道。这时可以用 !! 操作符强制将其视为非空。
对一个值使用 !! 会返回其非空类型;但如果该值实际为 null,会立即抛出 KotlinNullPointerException。
val name: String? = getNameFromAPI() // 假设你知道它不会返回 null
val upperName: String = name!!.uppercase()
这里 name!! 告诉编译器:“我保证 name 不是 null”,于是你可以直接调用 uppercase()。
但要极度谨慎:一旦判断错误,程序就会崩溃。因此,除非你有 100% 的把握,否则不要使用 !!。
常见场景与最佳实践
场景 1:从 API 获取数据
假设你从网络请求得到一个用户对象,某些字段可能缺失:
data class User(val name: String?, val email: String?)
安全处理方式:
val user: User? = fetchUser()
val greeting = "Hello, ${user?.name ?: "Anonymous"}!"
```
避免写成:
```kotlin
// 危险!如果 user 是 null,会崩溃
val greeting = "Hello, ${user!!.name}!"
场景 2:集合中的可空元素
val numbers: List<Int?> = listOf(1, null, 3)
val sum = numbers.map { it ?: 0 }.sum()
或者过滤掉 null:
val sum = numbers.filterNotNull().sum()
场景 3:平台类型(来自 Java)
当调用 Java 代码时,Kotlin 无法确定返回值是否可空,这类类型称为“平台类型”,显示为 String!。此时你有两个选择:
-
主动声明为可空类型,并使用安全调用:
val result: String? = javaObject.getName() val len = result?.length -
如果你确信它非空,可以赋值给非空类型(但内部仍可能崩溃):
val result: String = javaObject.getName() // 编译通过,但运行时可能 NPE
推荐做法是始终当作可空处理,除非有明确文档保证。
? 与 !! 对比总结
以下表格对比了两种操作符的核心差异:
| 特性 | ?. (安全调用) |
!! (非空断言) |
|---|---|---|
| 行为 | 若接收者为 null,返回 null | 若接收者为 null,抛出 KotlinNullPointerException |
| 返回类型 | 可空类型(如 T?) |
非空类型(如 T) |
| 安全性 | 安全,不会崩溃 | 危险,可能导致运行时崩溃 |
| 适用场景 | 处理不确定是否为 null 的值 | 你 100% 确定值非 null(如已检查过) |
如何避免滥用 !!
如果你发现自己频繁使用 !!,说明代码设计可能有问题。以下是替代方案:
-
使用 if 判断:
if (name != null) { println(name.length) // 在此作用域内,name 自动转为非空 } -
使用 let 函数:
name?.let { nonNullName -> println(nonNullName.length) } -
提前返回或抛出自定义异常:
fun processName(name: String?) { val validName = name ?: throw IllegalArgumentException("Name must not be null") // 后续使用 validName,它是非空的 }
这些方式比 !! 更清晰、更可控。
记住:Kotlin 的空安全不是障碍,而是保护伞。优先使用 ?. 和 ?:,把 !! 当作最后手段。

暂无评论,快来抢沙发吧!