跳到主要内容

Kotlin 空安全

空安全(Null Safety)是 Kotlin 最重要的特性之一,它在语言层面解决了困扰程序员的"十亿美元错误"——空指针异常(NullPointerException)。通过区分可空类型和不可空类型,Kotlin 在编译期就能捕获潜在的空指针问题。

可空类型与不可空类型

基本概念

在 Kotlin 中,类型系统明确区分可以持有 null 的类型(可空类型)和不可以持有 null 的类型(不可空类型):

fun main() {
// 不可空类型:默认情况下,类型不允许为 null
var name: String = "Kotlin"
// name = null // 编译错误:Null can not be a value of a non-null type String

// 可空类型:在类型后加 ? 表示可以为 null
var nullableName: String? = "Kotlin"
nullableName = null // 正确,可以为 null

println("name: $name")
println("nullableName: $nullableName")
}

理解要点

  • String 表示不可空字符串,永远不会有 null
  • String? 表示可空字符串,可能是 null
  • 编译器会在编译时检查类型安全,防止 NPE

可空类型的使用场景

fun main() {
// 1. 函数返回可空值
fun findUser(id: Int): String? {
return if (id > 0) "User$id" else null
}

val user = findUser(-1)
println(user) // null

// 2. 从 Map 获取值(键可能不存在)
val map = mapOf("a" to 1, "b" to 2)
val value: Int? = map["c"] // 返回 null,因为 "c" 不存在
println("value: $value")

// 3. 延迟初始化的场景
var loadedData: String? = null
println("加载前: $loadedData")
loadedData = "已加载数据"
println("加载后: $loadedData")
}

平台类型

与 Java 互操作时,Java 类型在 Kotlin 中被视为"平台类型",编译器无法确定其是否可空:

// 假设这是 Java 类
// public class JavaUser {
// public String getName() { return name; } // 可能返回 null
// }

fun main() {
// val javaUser = JavaUser()
// val name: String = javaUser.name // 可能 NPE!
// val name: String? = javaUser.name // 更安全的方式
}

最佳实践:与 Java 代码交互时,优先将返回值声明为可空类型,或者使用 @Nullable/@NotNull 注解。

安全调用操作符

?. 操作符

安全调用操作符 ?. 是处理可空类型最常用的方式。如果对象不为 null,则调用方法或访问属性;如果为 null,则返回 null

class Person(val name: String, val company: Company?)
class Company(val name: String)

fun main() {
val person = Person("Tom", Company("Google"))

// 安全调用
println(person.company?.name) // Google

// 如果 company 为 null,整个表达式返回 null
val person2 = Person("Jerry", null)
println(person2.company?.name) // null

// 链式安全调用
println(person.company?.name?.length) // 6
println(person2.company?.name?.length) // null
}

链式安全调用

安全调用操作符特别适合处理深层嵌套的可空属性:

class Address(val street: String?, val city: String?)
class Person(val name: String, val address: Address?)

fun main() {
// 各种可能的 null 情况
val person1 = Person("Tom", Address("Main St", "Beijing"))
val person2 = Person("Jerry", null)
val person3 = Person("Mike", Address(null, "Shanghai"))

// 链式安全调用:任一环节为 null,整体返回 null
println(person1.address?.city) // Beijing
println(person2.address?.city) // null
println(person3.address?.street) // null

// 更长的链式调用
println(person1.address?.city?.length) // 7
println(person2.address?.city?.length) // null
}

安全调用赋值

安全调用操作符也可以用在赋值语句的左侧:

class Person(var name: String? = null)

fun main() {
val person: Person? = Person("Tom")

// 安全调用赋值:如果 person 不为 null,则执行赋值
person?.name = "Jerry"
println(person?.name) // Jerry

// 如果 person 为 null,赋值操作会被跳过
val nullPerson: Person? = null
nullPerson?.name = "Test" // 不会执行
println(nullPerson?.name) // null(没有 NPE)
}

Elvis 操作符

?: 操作符

Elvis 操作符 ?: 用于提供默认值。如果左侧不为 null,返回左侧值;如果为 null,返回右侧值:

fun main() {
val name: String? = null

// 使用 Elvis 操作符提供默认值
val displayName = name ?: "Unknown"
println(displayName) // Unknown

// 当值不为 null 时
val name2: String? = "Kotlin"
val displayName2 = name2 ?: "Unknown"
println(displayName2) // Kotlin

// 常用场景:为可空值提供默认值
fun getLength(text: String?): Int {
return text?.length ?: 0
}

println(getLength("Hello")) // 5
println(getLength(null)) // 0
}

Elvis 与 return/throw

Elvis 操作符右侧可以是 returnthrow 等语句,用于提前退出:

fun main() {
// 与 return 结合
fun greet(name: String?) {
val displayName = name ?: return // 如果为 null,直接返回
println("Hello, $displayName!")
}

greet("Tom") // Hello, Tom!
greet(null) // 无输出,直接返回

// 与 throw 结合
fun process(input: String?) {
val value = input ?: throw IllegalArgumentException("Input cannot be null")
println("Processing: $value")
}

process("data") // Processing: data
// process(null) // 抛出异常
}

实际应用示例

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

fun main() {
fun getDisplayEmail(user: User?): String {
// 链式安全调用 + Elvis 操作符
return user?.email ?: "未设置邮箱"
}

fun getFullName(user: User?): String {
// 复杂的默认值逻辑
return user?.name ?: "用户${user?.id ?: "未知"}"
}

val user1 = User(1, "张三", "[email protected]")
val user2 = User(2, null, null)

println(getDisplayEmail(user1)) // [email protected]
println(getDisplayEmail(user2)) // 未设置邮箱
println(getFullName(user1)) // 张三
println(getFullName(user2)) // 用户2
}

非空断言操作符

!! 操作符

非空断言操作符 !! 将可空类型转换为不可空类型。如果值为 null,会抛出 NullPointerException

fun main() {
val name: String? = "Kotlin"

// 使用 !! 断言非空
println(name!!.length) // 6

// 如果值为 null,抛出 NPE
val name2: String? = null
// println(name2!!.length) // 抛出 KotlinNullPointerException
}

何时使用 !!

!! 操作符应该谨慎使用,只在以下情况考虑:

fun main() {
// 场景一:你确定值不为 null,但编译器无法推断
// 例如:延迟初始化后立即使用
lateinit var data: String
data = "initialized"
println(data) // 此时确定不为 null

// 场景二:框架保证非空
// 例如:某些 Android 框架方法在特定生命周期保证非空

// 场景三:测试代码中的快速失败
fun testNotNull() {
val result: String? = someFunction()
// 测试中希望快速发现 null 的情况
println(result!!.length)
}
}

fun someFunction(): String? = "test"

最佳实践:尽量避免使用 !!,优先使用 ?.?:!! 通常表示代码设计存在问题。

安全类型转换

as? 操作符

安全类型转换操作符 as? 尝试将值转换为指定类型,如果转换失败返回 null 而不是抛出异常:

fun main() {
val obj: Any = "Hello Kotlin"

// 普通类型转换(可能抛出 ClassCastException)
// val str: String = obj as String

// 安全类型转换
val str: String? = obj as? String
println(str) // Hello Kotlin

// 转换失败返回 null
val obj2: Any = 123
val str2: String? = obj2 as? String
println(str2) // null

// 结合 Elvis 操作符
val safeStr = obj2 as? String ?: "default"
println(safeStr) // default
}

实际应用

fun main() {
// 处理不同类型的输入
fun processValue(value: Any): String {
return when (value) {
is String -> "字符串: $value"
is Int -> "整数: $value"
is Double -> "浮点数: $value"
else -> "未知类型: $value"
}
}

// 安全解析数字
fun parseNumber(input: Any): Int? {
return when (val num = input as? Number) {
is Int -> num
is Double -> num.toInt()
is Float -> num.toInt()
is Long -> num.toInt()
else -> null
}
}

println(processValue("Hello")) // 字符串: Hello
println(processValue(42)) // 整数: 42
println(parseNumber(3.14)) // 3
println(parseNumber("abc")) // null
}

let 函数

基本用法

let 函数结合安全调用操作符,可以方便地处理可空值:

fun main() {
val name: String? = "Kotlin"

// 传统方式:显式检查
if (name != null) {
println(name.length)
}

// 使用 let
name?.let {
println("长度: ${it.length}") // 只有在 name 不为 null 时执行
println("内容: $it")
}

// name 为 null 时不执行
val name2: String? = null
name2?.let {
println("这行不会打印")
}
println("name2 为 null,let 块被跳过")
}

let 的优势

fun main() {
// 场景一:避免重复检查
fun processUser(user: User?) {
// 传统方式:需要多次检查
// if (user != null) {
// println(user.name)
// println(user.email)
// saveUser(user)
// }

// 使用 let:一次性处理
user?.let {
println("用户名: ${it.name}")
println("邮箱: ${it.email}")
saveUser(it)
}
}

// 场景二:链式调用
fun getDisplayName(user: User?): String {
return user?.let { "${it.name} (${it.id})" } ?: "未知用户"
}

// 场景三:作用域限制
fun process() {
val result = fetchData()
result?.let { data ->
// data 在此作用域内有效
processData(data)
saveData(data)
}
}
}

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

fun saveUser(user: User) {}
fun fetchData(): String? = null
fun processData(data: String) {}
fun saveData(data: String) {}

also、apply、run、with

这些作用域函数可以与空安全结合使用:

data class Person(var name: String? = null, var age: Int? = null)

fun main() {
// also:执行额外操作,返回原对象
val person1 = Person()
person1.also {
println("创建对象: $it")
}.apply {
name = "Tom"
age = 25
}
println(person1) // Person(name=Tom, age=25)

// apply:配置对象,返回原对象
val person2 = Person().apply {
name = "Jerry"
age = 30
}

// run:执行代码块,返回结果
val info = person1.run {
"$name, $age 岁"
}
println(info) // Tom, 25 岁

// with:对对象执行操作
with(person1) {
println("姓名: $name")
println("年龄: $age")
}

// 链式调用处理可空值
val result = person1
?.takeIf { it.age != null }
?.let { "${it.name} is ${it.age}" }
?: "信息不完整"
println(result)
}

可空类型集合

可空集合 vs 元素可空集合

fun main() {
// 可空集合:集合本身可以为 null
val nullableList: List<Int>? = null

// 元素可空集合:集合中的元素可以为 null
val listWithNulls: List<Int?> = listOf(1, null, 3, null, 5)

// 两者结合
val bothNullable: List<Int?>? = null

// 处理可空集合
println(nullableList?.size) // null
println(nullableList?.isEmpty()) // null

// 处理元素可空集合
println(listWithNulls.size) // 5
println(listWithNulls[1]) // null

// 过滤掉 null 元素
val filtered = listWithNulls.filterNotNull()
println(filtered) // [1, 3, 5]

// 计算非空元素
val sum = listWithNulls.filterNotNull().sum()
println(sum) // 9
}

常见操作

fun main() {
val list: List<String?> = listOf("a", null, "b", null, "c")

// filterNotNull:过滤 null 元素
println(list.filterNotNull()) // [a, b, c]

// mapNotNull:映射并过滤 null
val numbers = listOf("1", "abc", "2", null, "3")
val parsed = numbers.mapNotNull { it?.toIntOrNull() }
println(parsed) // [1, 2, 3]

// firstOrNull / lastOrNull
println(list.firstOrNull { it != null }) // a
println(list.firstNotNullOfOrNull { it?.length }) // 1
}

智能类型转换

Kotlin 编译器会自动跟踪空检查,进行智能类型转换:

fun main() {
val str: String? = "Hello"

// 编译器知道在 if 块内 str 不为 null
if (str != null) {
// str 被智能转换为 String(非空)
println(str.length) // 不需要 ?. 或 !!
}

// 复杂条件也支持
val a: String? = "A"
val b: String? = "B"

if (a != null && b != null) {
// a 和 b 都被智能转换为非空
println("$a and $b")
}

// 注意:智能转换只对 val 有效
var mutableStr: String? = "Hello"
if (mutableStr != null) {
// mutableStr 可能被其他地方修改,不保证智能转换
// println(mutableStr.length) // 可能编译错误
// 需要:
println(mutableStr?.length)
}
}

最佳实践

1. 优先使用非空类型

// 推荐:使用非空类型
fun greet(name: String) {
println("Hello, $name!")
}

// 不推荐:过度使用可空类型
fun greet2(name: String?) {
println("Hello, ${name ?: "Guest"}!")
}

2. 使用 lateinit 延迟初始化

class MyService {
// 延迟初始化非空属性
private lateinit var database: Database

fun init() {
database = Database.connect()
}

fun query(): String {
// 检查是否已初始化
if (::database.isInitialized) {
return database.query()
}
return "Database not initialized"
}
}

class Database {
companion object {
fun connect() = Database()
}
fun query() = "result"
}

3. 使用 require 和 check

fun main() {
// require:检查参数
fun process(input: String?) {
requireNotNull(input) { "Input cannot be null" }
// input 被智能转换为非空
println(input.length)
}

// check:检查状态
class Processor {
private var initialized = false

fun init() {
initialized = true
}

fun process() {
check(initialized) { "Must call init() first" }
println("Processing...")
}
}

// process(null) // 抛出 IllegalArgumentException
process("data") // 4

val processor = Processor()
// processor.process() // 抛出 IllegalStateException
processor.init()
processor.process()
}

4. 避免 !! 操作符

// 不推荐
fun getNameLength(name: String?): Int {
return name!!.length // 危险!
}

// 推荐
fun getNameLengthSafe(name: String?): Int {
return name?.length ?: 0
}

// 或者
fun getNameLengthSafe2(name: String?): Int? {
return name?.length
}

5. 合理使用可空性注解

// 与 Java 互操作时,使用注解明确可空性
// Java:
// @Nullable String getName();
// @NotNull String getEmail();

// Kotlin 可以正确识别:
// val name: String? = javaObj.name
// val email: String = javaObj.email

小结

本章我们学习了 Kotlin 的空安全特性:

  1. 可空类型Type? 表示可以为 null 的类型
  2. 安全调用?. 在对象非空时调用方法
  3. Elvis 操作符?: 提供默认值
  4. 非空断言!! 强制转换(谨慎使用)
  5. 安全转换as? 安全类型转换
  6. let 函数:在非空时执行代码块
  7. 智能类型转换:编译器自动处理空检查
  8. 最佳实践:优先使用非空类型,避免 !!

Kotlin 的空安全机制是语言级别的设计,让 null 问题在编译期就能被发现,大大减少了运行时的空指针异常。

练习

  1. 创建一个函数,接受可空字符串参数,返回其大写形式(null 时返回空字符串)
  2. 使用 let 函数处理可空用户对象,打印其信息
  3. 使用 Elvis 操作符和 throw 结合,实现参数验证
  4. 创建一个包含可空属性的 Person 类,使用链式安全调用获取深层属性
  5. 重构代码,将所有 !! 替换为更安全的空处理方式