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 操作符右侧可以是 return、throw 等语句,用于提前退出:
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 的空安全特性:
- 可空类型:
Type?表示可以为null的类型 - 安全调用:
?.在对象非空时调用方法 - Elvis 操作符:
?:提供默认值 - 非空断言:
!!强制转换(谨慎使用) - 安全转换:
as?安全类型转换 - let 函数:在非空时执行代码块
- 智能类型转换:编译器自动处理空检查
- 最佳实践:优先使用非空类型,避免
!!
Kotlin 的空安全机制是语言级别的设计,让 null 问题在编译期就能被发现,大大减少了运行时的空指针异常。
练习
- 创建一个函数,接受可空字符串参数,返回其大写形式(
null时返回空字符串) - 使用
let函数处理可空用户对象,打印其信息 - 使用 Elvis 操作符和
throw结合,实现参数验证 - 创建一个包含可空属性的
Person类,使用链式安全调用获取深层属性 - 重构代码,将所有
!!替换为更安全的空处理方式