空安全与类型转换
本教程共 100 篇 · 第 16 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
16. 空安全与类型转换
本节目标:彻底搞懂 Kotlin 怎么处理 null、安全调用怎么写、Elvis 运算符、作用域函数(let/also/run/apply/with/takeIf)怎么选、类型检查与智能转换怎么做。
为什么空安全重要
NullPointerException(NPE)是 Java 应用最常见的崩溃,被戏称为「十亿美元错误」。Kotlin 在类型系统层面把「可空」和「非空」区分开,把大量 NPE 挪到编译期解决。
可空类型
默认类型不接受 null,要存 null 加 ?:
var name: String = "Android"
// name = null // 编译报错
var nickname: String? = "Andy"
nickname = null // OK
安全调用 ?.
可空类型用 . 直接访问成员会报错,要用安全调用 ?.:
val len = nickname?.length // nickname 不为 null 返回 length,为 null 返回 null
// 类型是 Int?
链式安全调用:
val city = user?.address?.city // 任何一环为 null,整体返回 null
Elvis 运算符 ?:
?: 给 null 提供默认值:
val len = nickname?.length ?: 0 // 为 null 时用 0
val displayName = nickname ?: "匿名" // 为 null 时用"匿名"
// 也常用于提前返回
val user = getUser() ?: return // 为 null 直接 return
val data = response?.data ?: throw IllegalStateException("无数据")
Tip
- Elvis 在 Android 里特别有用:取 Intent extra 给默认值、解析 JSON 给默认值、UI 显示空字符串代替 null,都用它。
非空断言 !!
!! 强制把可空转非空,如果真是 null 就抛 NPE:
val len = nickname!!.length // 断言 nickname 非 null
Warning
- 慎用
!!。它等于放弃了 Kotlin 的空安全保护,把 NPE 重新请回来。只在「逻辑上确定不为 null 但类型是可空」时用,并加注释说明。能用?:给默认值或?.let { }处理的就别用!!。
安全转换 as?
as 强转类型不匹配会抛 ClassCastException,as? 失败时返回 null:
val obj: Any = "hello"
val len: Int? = obj as? Int // null,不会崩
val str: String? = obj as? String // "hello"
配 Elvis 给默认值:
val num = (obj as? Int) ?: 0
类型检查 is / !is
if (obj is String) {
// 在这个分支里 obj 自动是 String 类型,不用强转
println(obj.length)
}
if (obj !is String) return
// 这里 obj 也是 String
println(obj.length)
is 判断后编译器自动做智能转换(Smart Cast),不用手动强转。这是 Kotlin 比 Java 顺手的地方。
Note
- 智能转换只对
val或经编译器确认不会变的变量有效。var在多线程下可能被改,智能转换不生效,要手动as。
作用域函数
Kotlin 有 6 个作用域函数,用来简化对对象的操作。它们区别在引用方式和返回值:
| 函数 | 引用方式 | 返回值 | 典型场景 |
|---|---|---|---|
let | it | Lambda 结果 | 可空处理、转换 |
run | this | Lambda 结果 | 初始化并返回结果 |
with | this | Lambda 结果 | 对同一对象多次操作 |
apply | this | 对象本身 | 配置对象(Builder 模式) |
also | it | 对象本身 | 副作用(日志、调试) |
takeIf | it | 对象或 null | 条件过滤 |
let
最常用于可空处理:
nickname?.let {
// 只有 nickname 非 null 才执行,it 是非空的 String
println("名字长度 ${it.length}")
}
// 转换
val length = name?.let { it.length } ?: 0
apply
配置对象,返回对象本身,适合 Builder 模式:
val intent = Intent().apply {
action = "android.intent.action.VIEW"
data = Uri.parse("https://example.com")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val dialog = AlertDialog.Builder(context).apply {
setTitle("提示")
setMessage("确定删除吗?")
setPositiveButton("确定") { _, _ -> /* 删除 */ }
}.create()
also
做副作用,不影响链式调用:
val user = User("张三", 18).also {
log("创建了用户:${it.name}") // 打日志,返回 user
}
run / with
// run:初始化并返回结果
val greeting = StringBuilder().run {
append("Hello, ")
append(name)
append("!")
toString()
}
// with:对同一对象多次操作
with(textView) {
text = "Hello"
setTextColor(Color.RED)
textSize = 16f
}
takeIf / takeUnless
条件过滤:
val file = File("config.json")
val valid = file.takeIf { it.exists() }?.takeIf { it.length() > 0 }
// file 不存在或为空,valid 为 null
val even = num.takeIf { it % 2 == 0 } // 偶数返回自身,奇数返回 null
Tip
- 记不住没关系,记住两个最常用:可空用
let,配置对象用apply。其他用到再查。
Android 实战
// 处理 Intent extra
val userId: Long = intent.getLongExtra("user_id", 0L)
val name: String = intent.getStringExtra("name") ?: "匿名"
val avatar: String? = intent.getStringExtra("avatar") // 可能没传
// 处理网络响应
response.body()?.let { body ->
showData(body)
} ?: run {
showError("响应为空")
}
// Activity 里 findViewById 配合 apply
val textView = TextView(context).apply {
text = "Hello"
setPadding(16, 16, 16, 16)
}
// ViewModel 处理可空状态
fun getUser(id: String) {
viewModelScope.launch {
val user = repo.getUser(id) // 返回 User?
user?.let {
_state.value = UiState.Success(it)
} ?: run {
_state.value = UiState.Error("用户不存在")
}
}
}
空安全的常见陷阱
1. 平台类型
调 Java 代码时,Java 返回的类型没有可空信息,Kotlin 把它当「平台类型」(String!),可能为 null 也可能不为 null。
// Java 方法返回 String,Kotlin 调用
val name = javaObj.getName() // 类型是 String!,不确定可空
println(name.length) // 如果 name 实际是 null,NPE
Warning
- 调 Java API 返回的值,要么当可空处理(
?.),要么明确赋给可空类型变量。别直接当非空用。
2. lateinit 延迟初始化
有些属性(如 Activity 里的 View)不能在构造时初始化,又不想用可空类型。用 lateinit:
class MainActivity : AppCompatActivity() {
private lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.button) // 这里初始化
button.setOnClickListener { /* ... */ }
}
}
lateinit 只能用于 var,不能是基本类型。访问前没初始化会抛 UninitializedPropertyAccessException。
判断是否初始化(仅 lateinit var 支持):
if (::button.isInitialized) {
button.setOnClickListener(null)
}
3. lazy 惰性初始化
只读属性第一次访问时才初始化:
val database by lazy { Room.databaseBuilder(this, AppDb::class.java, "app.db").build() }
lazy 是线程安全的(默认 LazyThreadSafetyMode.SYNCHRONIZED),单例初始化常用。
小结
类型默认非空,加 ? 才能存 null。?. 安全调用,?: 给默认值,!! 谨慎用。as? 安全转换,is 后智能转换。作用域函数记住 let(可空处理)和 apply(配置对象)。调 Java 代码注意平台类型。lateinit 和 lazy 处理延迟初始化。下一节学高阶函数与 Lambda。