视若飞
1、Kotlin中延迟方法有多少种?
在 Kotlin 中实现“延迟”有多种方式,根据其设计目标和应用场景,可以系统地分为以下几类。你可以通过下图快速了解它们的核心区别与关联:
2、线程阻塞延迟:Thread.sleep(millis)
这是来自 Java 的标准方法,会阻塞当前线程。
- 特点:简单粗暴,但会让线程停止所有工作,在协程或主线程中使用会导致性能问题或 ANR。
- 使用场景:普通线程编程或测试中,在协程中应避免使用。
Thread.sleep(1000L) // 当前线程休眠1秒
3、协程非阻塞延迟:delay(millis)
这是 Kotlin 协程库 中的挂起函数,是协程中实现延迟的首选和标准方式。
- 特点:挂起当前协程,但释放底层线程供其他协程使用,不会阻塞线程,高效且符合协程哲学。
- 使用场景:任何需要在协程中等待一段时间的场景。
suspend fun fetchData() {
// 延迟1秒,但不阻塞线程
delay(1000L)
// ... 执行网络请求
}
4、属性延迟初始化:lazy 委托
这是 Kotlin 标准库 中的属性委托,用于值的惰性求值。
- 特点:属性只在首次访问时执行初始化 lambda 表达式,并将结果缓存起来供后续访问。这是针对状态的延迟,而非时间上的延迟。
- 使用场景:初始化开销大、可能不使用的属性。
val expensiveResource: Resource by lazy {
println("首次访问时初始化!")
Resource() // 这个初始化操作只会执行一次
}
// 第一次调用 expensiveResource 时,才会执行初始化
5、协程延迟启动:CoroutineStart.LAZY
这是协程的一种启动模式,用于控制协程的执行时机。
- 特点:使用
launch或async创建协程时不会立即执行,直到手动调用start()/join()(对Job) 或await()(对Deferred)。 - 使用场景:需要定义但不立即执行协程,或构建复杂的协程依赖关系。
val lazyJob = launch(start = CoroutineStart.LAZY) {
println("这个协程不会立即运行")
}
// ... 在某个时刻
lazyJob.start() // 此时协程才开始执行
6、异步结果的延迟获取:async 与 Deferred
这是结构化并发中用于并发执行并延迟获取结果的模式。
- 特点:
async启动一个协程并立即返回一个Deferred对象(类似Future)。你可以在需要结果时才调用Deferred.await()来获取(这可能会挂起等待结果)。 - 使用场景:并行执行多个任务,并在之后组合它们的结果。
suspend fun fetchTwoThings() {
val deferred1 = async { fetchData1() } // 立即开始异步执行
val deferred2 = async { fetchData2() } // 立即开始异步执行
// 在需要结果时才等待,两者并发执行
val result1 = deferred1.await() // 挂起直到fetchData1完成
val result2 = deferred2.await() // 如果已完成则立即返回
}
7、 核心对比与选择指南
| 方法 | 所属库 | 核心目的 | 是否会阻塞线程? | 主要使用场景 |
|---|---|---|---|---|
Thread.sleep() | Java标准库 | 让线程休眠指定时间 | 是 | 线程测试、传统线程编程。协程中避免使用。 |
delay() | 协程库 (kotlinx.coroutines) | 让协程非阻塞地挂起指定时间 | 否 | 协程中实现延迟的标准方式,如实现重试、防抖。 |
lazy 委托 | Kotlin标准库 | 属性的惰性初始化(延迟计算) | 不涉及 | 延迟初始化开销大的单例或属性。 |
CoroutineStart.LAZY | 协程库 | 控制协程启动的时机 | 不直接涉及 | 按需启动协程任务,构建任务图。 |
async/Deferred | 协程库 | 异步计算并延迟获取结果 | 否(await是挂起) | 并发执行任务,最后聚合结果。 |
面试回答要点
当被问到这个问题时,不要只列举方法,而要体现分类思维:
- 首先区分维度:“延迟”在Kotlin中主要有三个维度:时间的延迟(
sleep/delay)、初始化的延迟(lazy)、执行的延迟(LAZY启动模式)。 - 强调协程首选:在协程中,时间延迟绝对应该使用
delay()而非Thread.sleep(),因为前者是协作式的,不阻塞线程。 - 澄清易混概念:特别说明
lazy是属性委托,用于初始化;而CoroutineStart.LAZY是启动选项,用于控制协程执行时机。两者名字相似但用途完全不同。 - 提及高级模式:可以补充
async/Deferred模式,它实现了结果的延迟获取,是并发编程的重要工具。
总之,Kotlin 提供了不同层面的“延迟”工具,选择哪一种取决于你是要延迟时间、延迟初始化还是延迟执行。
8、Kotlin中 async+wait是有结果的等待,没有结果的等待是什么方法?
在 Kotlin 协程中,如果你需要等待一个没有返回结果的并发任务完成,应该使用 launch 构建器配合 Job.join() 方法。
核心机制:Job.join()
launch 启动一个协程,它返回一个 Job 对象。Job.join() 是一个挂起函数,它会挂起当前协程,直到这个 Job 代表的任务完成(包括其所有子协程)。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("主协程开始")
// launch 启动一个没有返回结果的任务
val job = launch {
delay(1000L) // 模拟一些工作
println("后台任务完成!")
}
println("主协程继续执行其他操作...")
// 等待 job 完成,没有返回值
job.join()
println("主协程确认后台任务已结束,继续执行")
}
// 输出:
// 主协程开始
// 主协程继续执行其他操作...
// (等待约1秒)
// 后台任务完成!
// 主协程确认后台任务已结束,继续执行
9、与 async/await 的详细对比
| 方面 | launch + Job.join() (无结果等待) | async + Deferred.await() (有结果等待) |
|---|---|---|
| 构建器 | launch | async |
| 返回对象 | Job (代表一个任务的生命周期) | Deferred<T> (继承自 Job,代表一个未来会产生 T 类型结果的任务) |
| 等待方法 | Job.join() | Deferred.await() |
| 方法返回值 | 无 (Unit) | 有 (类型 T) |
| 主要目的 | 执行副作用 (如更新状态、发送日志、启动服务),关注任务完成时机。 | 执行计算并获取结果,关注任务产出值。 |
| 类比 | 就像让助手去寄一封信,你只需要等他回来告诉你“办好了”。 | 就像让助手去计算一个复杂公式,你需要等他回来并拿到计算结果。 |
10、实际应用场景与代码示例
场景1:等待多个并行任务全部完成
这是最常见的用例,比如同时上传多张图片。
suspend fun uploadAllImages(imageList: List<Image>) {
val jobs = mutableListOf<Job>()
imageList.forEach { image ->
// 每个上传任务都不需要返回结果给调用者,只需确保完成
val job = launch {
uploadImage(image) // 假设这是一个挂起函数
}
jobs.add(job)
}
// 等待所有启动的任务完成
jobs.joinAll() // 这是对 List<Job> 的扩展函数,内部调用了每个job的join()
println("所有图片上传完成!")
}
场景2:结构化并发中的隐式等待
在结构化并发中,父协程默认会等待其所有子协程完成,你通常不需要显式调用 join()。但在某些需要精细控制流程时,仍然会用到。
fun main() = runBlocking {
// 父协程
launch {
// 子协程A
val jobA = launch {
delay(500)
println("A完成")
}
// 子协程B
val jobB = launch {
delay(1000)
println("B完成")
}
// 这里父协程可以执行自己的逻辑,然后显式等待某个子协程
println("父协程做点别的事...")
jobA.join() // 显式等待A完成
println("知道A完成后,再触发一些操作")
// 不需要显式等待jobB,因为父协程结束时(这里隐式)会自动等待所有子协程
}.join() // 等待整个父协程(及其所有子协程)完成
println("全部结束")
}
常见误区
- 用
async但不调用await:如果你用async启动协程却从不调用await,你就无法获取结果,也无法感知任务中的异常(因为异常会存储在Deferred对象中,直到await被调用时才抛出)。所以对于“没有结果”的任务,请直接用launch。 - 在
launch块中返回无意义值:不要这样写:async { doWork(); Unit }.await(),这浪费了async创建Deferred对象的开销,直接用launch { doWork() }.join()更清晰高效。
当被问到与 async/await 的对比时,可以这样回答:
“对于不需要返回结果的并发任务,Kotlin 协程使用
launch构建器来启动,它返回一个Job对象。通过调用这个Job的join()挂起函数,我们可以等待该任务完成。这与async(返回Deferred)后调用await()来获取结果的模式相对应。join()和await()都是挂起函数,但前者只同步完成状态,后者同步结果值。在结构化并发中,父协程自动等待子协程,但join()在需要精确控制多个并发子任务完成顺序时非常有用。”
11、Kotlin 协程怎么封装网络请求的?
/**
* 过滤服务器结果,失败抛异常
* @param block 请求体方法,必须要用suspend关键字修饰
* @param success 成功回调
* @param error 失败回调 可不传
* @param isShowDialog 是否显示加载框
* @param loadingMessage 加载框提示内容
*/
fun <T> BaseViewModel.request(
block: suspend () -> BaseResponse<T>,
success: (T) -> Unit,
error: (AppException) -> Unit = {},
isShowDialog: Boolean = false,
loadingMessage: String = "请求网络中..."
): Job {
//如果需要弹窗 通知Activity/fragment弹窗
return viewModelScope.launch {
runCatching {
if (isShowDialog) loadingChange.showDialog.postValue(loadingMessage)
//请求体
block()
}.onSuccess {
//网络请求成功 关闭弹窗
loadingChange.dismissDialog.postValue(false)
runCatching {
//校验请求结果码是否正确,不正确会抛出异常走下面的onFailure
executeResponse(it) { t -> success(t)
}
}.onFailure { e ->
//打印错误消息
e.message?.loge()
//打印错误栈信息
e.printStackTrace()
//失败回调
error(ExceptionHandle.handleException(e))
}
}.onFailure {
//网络请求异常 关闭弹窗
loadingChange.dismissDialog.postValue(false)
//打印错误消息
it.message?.loge()
//打印错误栈信息
it.printStackTrace()
//失败回调
error(ExceptionHandle.handleException(it))
}
}
}
/**
* 请求结果过滤,判断请求服务器请求结果是否成功,不成功则会抛出异常
*/
suspend fun <T> executeResponse(
response: BaseResponse<T>,
success: suspend CoroutineScope.(T) -> Unit
) {
coroutineScope {
when {
response.isSucces() -> {
success(response.getResponseData())
}
else -> {
throw AppException(
response.getResponseCode(),
response.getResponseMsg(),
response.getResponseMsg()
)
}
}
}
}
这种封装遵循“关注点分离”原则,将以下通用逻辑从业务代码中抽离:
- 协程生命周期管理:自动在
viewModelScope中启动。 - 加载状态管理:统一处理加载对话框的显示与隐藏。
- 异常处理与转换:将各种异常(网络、解析、业务)转换为统一的
AppException传递给UI层。 - 响应体校验:统一检查服务器返回的业务状态码。
我们来拆解这个 request 函数的每个部分:
| 代码部分 | 设计目的与原理 | 带来的好处 |
|---|---|---|
fun <T> BaseViewModel.request(...): Job | 1. 泛型 <T> :支持任意类型的响应数据。 2. 定义在 BaseViewModel:所有子ViewModel都可复用。 3. 返回 Job:允许调用者在需要时取消请求。 | 类型安全、代码复用、可控性。 |
block: suspend () -> BaseResponse<T> | 核心的请求执行体。是一个挂起函数,由调用者传入具体的请求(如 repository.getUser())。 | 解耦:封装器不关心具体请求,只负责执行和包装流程。 |
success: (T) -> Unit | 成功回调。接收从 BaseResponse 中提取的真正的业务数据 T。 | 业务层只关注最终有用的数据,无需处理响应包装。 |
error: (AppException) -> Unit | 失败回调。接收经过统一处理的、面向UI的异常。 | UI层得到友好、统一的错误信息,可直接用于提示。 |
isShowDialog 与 loadingChange | 通过 loadingChange(可能是一个 LiveData)通知UI层显示/隐藏加载框。 | UI状态控制逻辑被模板化,业务ViewModel无需重复编写。 |
viewModelScope.launch | 在ViewModel的协程作用域中启动,确保请求在ViewModel销毁时自动取消,避免内存泄漏。 | 生命周期安全,这是Android协程开发的最佳实践。 |
runCatching { ... } | Kotlin标准库函数,优雅地捕获块内所有异常,替代传统的 try-catch。 | 使成功/失败的逻辑分支非常清晰。 |
executeResponse(it) { t -> success(t) } | 关键步骤:解析 BaseResponse,校验业务码。成功则提取 T 并回调 success;失败则抛出业务异常,由外层 onFailure 捕获。 | 将“网络成功”和“业务成功”分离。业务错误(如 token 过期)也能走统一的错误流程。 |
ExceptionHandle.handleException(it) | 统一异常处理枢纽:将 Throwable(可能是IO异常、解析异常、自定义业务异常)转换为前端UI可理解的 AppException(包含错误消息和错误码)。 | 异常处理标准化,UI层只需处理一种异常类型。 |
| 优势总结 |
- 高度复用,代码精简:业务ViewModel代码极简,只需一行
request调用。 - 统一管理,行为一致:所有网络请求的加载、错误处理方式一致。
- 安全可靠:依托
viewModelScope,自动避免生命周期问题。 - 易于测试:
block参数易于用模拟对象替换,方便单元测试。
12、Kotlin 协程怎么处理异常的?
核心规则与机制
-
异常会传播:协程中未捕获的异常会向其父协程传播,导致父协程取消,进而取消其所有子协程(这是结构化并发取消的一部分)。
-
两种传播方式:
launch构建器:异常会立即在发生处抛出,并开始传播。async构建器:异常会延迟到您调用.await()获取结果时才抛出。
13、Kotlin协程异常的四大处理方式与代码示例
方式一:直接 try-catch(最基础)
直接在可能发生异常的代码块周围使用 try-catch。这对于 launch 和 async 都有效。
scope.launch {
try {
fetchData() // 挂起函数,可能抛出异常
} catch (e: Exception) {
// 处理异常,例如更新UI显示错误
showError(e.message)
}
}
// 对于 async,必须在 await() 处捕获
scope.launch {
val deferred = async { fetchData() }
try {
val result = deferred.await()
process(result)
} catch (e: Exception) {
// 处理来自 async 块的异常
}
}
方式二:CoroutineExceptionHandler(全局捕获)
这是一个上下文元素,用于在协程的根节点(通常是顶级或 SupervisorJob 下的直接子协程)捕获未处理的异常。它就像是协程世界的“全局 Thread.uncaughtExceptionHandler”。
// 1. 定义异常处理器
val handler = CoroutineExceptionHandler { _, exception ->
println("协程上下文捕获到未处理异常: $exception")
// 可以在这里上报崩溃日志
}
// 2. 在作用域中使用(注意:必须在根协程或 SupervisorJob 的直接子协程才有效)
val scope = CoroutineScope(Job() + handler) // 作为根作用域的一部分
scope.launch {
// 如果这里发生异常,会被 handler 捕获
throw RuntimeException("测试异常")
}
// 以下方式无效!异常会直接崩溃,因为handler不是根协程的
scope.launch {
launch(handler) { // ❌ 错误:handler 不是这个 launch 的根
throw RuntimeException("这个异常 handler 抓不到!")
}
}
方式三:SupervisorJob / supervisorScope(隔离异常)
这是防止“一个失败,全家遭殃” 的关键。默认情况下,父协程使用普通 Job,一个子协程失败会取消所有兄弟协程。SupervisorJob 改变了这一规则,使子协程的失败彼此隔离。
// 1. 使用 SupervisorJob 创建作用域
val supervisorScope = CoroutineScope(SupervisorJob())
supervisorScope.launch {
delay(1000)
throw RuntimeException("子协程1失败") // 这个会崩溃或由handler处理
}
supervisorScope.launch {
delay(2000)
println("子协程2仍然会执行!") // 尽管协程1失败了,但协程2不受影响
}
// 2. 使用 supervisorScope 构建器(更常用)
suspend fun handleMultipleTasks() = supervisorScope { // 进入一个监督作用域
val child1 = launch {
throw RuntimeException("任务A失败")
}
val child2 = launch {
delay(500)
println("任务B正常完成") // 任务A的失败不会取消任务B
}
// 可以等待所有子协程,并单独处理它们的失败
child1.join()
child2.join()
}
方式四:在 async 块内部捕获并返回结果状态
这是处理 async 异常的推荐模式,可以避免在 await() 时抛出异常,而是返回一个包含成功/失败信息的结果类。
// 定义结果密封类
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
}
suspend fun fetchDataSafely(): Result<Data> = withContext(Dispatchers.IO) {
return@withContext try {
Result.Success(apiService.getData()) // 假设 getData() 是 suspend fun
} catch (e: Exception) {
Result.Error(e)
}
}
// 在调用处,无需 try-catch await()
scope.launch {
val deferred = async { fetchDataSafely() }
when (val result = deferred.await()) { // 这里不会抛出异常
is Result.Success -> updateUI(result.data)
is Result.Error -> showError(result.exception)
}
}
14、协程的异常处理回答
当被问到时,可以这样结构化回答:
-
先讲核心哲学:“协程的异常处理遵循结构化并发原则,未处理异常会向上传播并取消整个协程作用域,这保证了程序的健壮性。”
-
分两点阐述传播差异:
- “
launch启动的协程,异常会立即传播,通常需要用try-catch或CoroutineExceptionHandler在根协程捕获。” - “
async启动的协程,异常会延迟到调用await()时才抛出,必须在await()处处理,更好的做法是在async块内部封装成Result类返回。”
- “
-
强调关键工具
SupervisorJob:“为了防止一个子任务失败导致整个作用域取消,我们需要使用SupervisorJob或supervisorScope,这在处理并行的、独立的任务时(如同时上传多张图片)至关重要。” -
给出最佳实践:“在生产代码中,我们通常会:
- 为根协程作用域配置
CoroutineExceptionHandler来兜底未捕获异常,并上报日志。 - 对于并行的独立任务,使用
supervisorScope。 - 对于需要结果的异步计算,使用
async并在内部捕获异常,返回Result密封类。 - 始终记得,在 Android 的
viewModelScope或lifecycleScope中,它们默认使用了SupervisorJob,所以一个屏幕内的多个协程失败不会相互影响。”
- 为根协程作用域配置
15、MMKV 内部是怎么实现的?
1. 基石:内存映射 (mmap)
-
操作文件就像操作数组一样。
-
关键优势:
2. 高效序列化:Protocol Buffer (protobuf)
MMKV使用protobuf对键值对进行二进制编码
。相比SharedPreferences的XML格式,protobuf编码更紧凑、序列化/反序列化速度更快,这也是MMKV高性能和节省空间的重要原因。
3. 核心设计:Append-Only(追加写)与增量更新
这是MMKV区别于SharedPreferences全量更新的核心。
- 这确保了每次写入都是顺序、单次I/O操作,极其高效。
- 索引维护:为了能快速定位数据,MMKV在内存中维护了一个哈希表,记录每个Key对应数据在内存映射区域中的起始位置和大小。每次追加新数据后,这个内存索引会同步更新。
4. 空间回收:文件重整与扩容
“只追加不删除”的模式会导致文件不断增大,并产生大量过期数据。MMKV通过以下机制解决:
- 文件重整:当文件大小超出预期或过期数据太多时,MMKV会进行一次重整。遍历有效数据,将其重新紧凑地写入一个新文件(或原文件的前部),然后替换旧文件。这个过程去除了所有冗余数据。
- 动态扩容:当可用空间不足且重整后仍无法满足时,MMKV会进行扩容(通常是将文件扩大一倍),以满足后续写入。
5. 多进程与线程安全
总结:MMKV的三大优势
- mmap技术:保证了I/O的高性能和崩溃数据不丢失。
- protobuf编码:实现了高效序列化和紧凑存储。
- 追加写与增量更新:彻底避免了全量写入,让每次写入都轻量而快速。
16、SharedPrefrence 做了哪些优化?
haredPreferences(简称SP)的核心优化围绕着解决卡顿、避免ANR、提升稳定性展开。经过Google多年的演进(特别是从API 14到API 28的 apply() 引入,以及新版 PreferenceDataStore),它的主要优化点如下:
| 优化维度 | 具体优化措施 | 解决的问题与原理 |
|---|---|---|
| 🚀 性能优化 | 1. 异步加载 (getSharedPreferences) | 启动优化:首次调用时在子线程执行文件I/O和XML解析,避免在主线程卡顿。后续调用直接返回内存缓存。 |
| 2. 应用启动预加载 | 进一步优化启动:App启动时自动预加载默认SP文件,让后续首次调用几乎零耗时。 | |
3. apply() 异步写入 | 取代commit() :将写文件操作放入子线程队列,避免同步I/O阻塞主线程(核心防ANR优化)。 | |
4. commit() 优化 | 多commit合并:在主线程调用时,会合并多个commit,减少等待和文件系统调用次数。 | |
| 🛡️ 稳定性与数据安全 | 1. 原子写入(新版) | 防止数据损坏:通过“写入新文件 → 删除旧文件 → 重命名”的原子操作,确保异常情况下至少有一份完整数据。 |
| 2. 崩溃恢复(新版) | 增强容错:读取时若检测到文件损坏,会自动尝试恢复备份或重新创建。 | |
3. apply() 可靠性增强 | 确保最终持久化:新版会等待apply的异步写入完成,或至少在Activity.onPause()等生命周期节点强制同步,防止数据丢失。 | |
| 4. 多进程模式改良 | 有限支持:MODE_MULTI_PROCESS已废弃,新版通过文件锁和检查机制提供更安全但仍有延迟的多进程基础。 | |
| 💡 其他优化 | 1. 内存缓存 (sSharedPrefsCache) | 提升读取速度:所有SP实例以 Map<String, Map<...>> 结构进行内存缓存,读取如同操作 HashMap。 |
| 2. 数据类型优化 | 减少解析开销:原生支持int, long, float, boolean, String, Set<String>,避免不必要的序列化。 |
17、 SharedPrefrence总结与最佳实践
虽然经过优化,但SP的设计(XML全量读写、内存全量缓存)决定了其本质仍不适合存储大量数据或频繁更新。
面试回答要点:
当被问到“SharedPreferences做了哪些优化?”时,可以按以下逻辑回答:
- 核心防ANR优化:强调
apply()机制,说明其异步写入如何避免主线程卡顿。 - 性能优化:提及异步加载、内存缓存、
commit合并,减少I/O和等待。 - 数据安全优化:说明原子写入和崩溃恢复机制如何保证数据完整性。
- 指出局限性:说明其仍存在的全量更新、不适合大量数据、多进程不可靠等问题,并引出
DataStore或MMKV等现代替代方案。
实际开发建议:
- 场景:仅用于存储少量、非关键、低频更新的简单配置(如用户设置开关、界面主题)。
- 禁忌:不要存储JSON字符串、大字符串、频繁更新的计数器、敏感信息。
- 方法:始终使用
apply(),除非你必须立即知道写入结果(这种情况极少)。 - 替代:对于复杂数据、高性能或跨进程需求,考虑使用
DataStore(官方推荐)、MMKV或SQLite。
总之,SharedPreferences的优化使其在特定场景下更安全可用,但其架构限制决定了它并非现代数据存储的最佳选择。
18、runCatching { ... } 和try-catch的区别?
runCatching { ... }和传统的try-catch都是为了处理异常,但它们的核心思想、代码风格和适用场景有根本区别。简单来说:runCatching是 “函数式” 的风格,把异常封装成一个可传递的结果;而try-catch是 “命令式” 的风格,立即中断流程并进行处理。
为了方便你快速理解核心区别,我将它们的主要差异总结在下表中:
| 维度 | runCatching { ... } | 传统 try-catch |
|---|---|---|
| 核心机制 | 函数式、声明式。将执行块和异常都封装在 Result 对象中。 | 命令式、过程式。直接中断流程并跳转到 catch 块。 |
| 返回值 | 始终返回一个 Result<T> 对象(成功包含值,失败包含异常)。 | 没有固定返回值,依赖在块内赋值或直接处理。 |
| 代码风格 | 支持链式调用(.onSuccess、.onFailure、.map等),流程清晰。 | 代码呈块状结构,成功与失败逻辑被物理分隔。 |
| 异常处理时机 | 延迟处理。异常被安全存储,你可以在链式调用的最后决定何时、如何处理。 | 立即处理。异常抛出后必须立即在 catch 块中处理。 |
| 组合能力 | 强。可以轻松地与 map、recover 等其他函数式操作符组合,进行转换或恢复。 | 弱。难以直接与其他操作组合,嵌套复杂逻辑时代码易乱。 |
| 作用范围 | 仅作用于其lambda表达式块内发生的异常。 | 作用于整个 try 块内(可能包含多行无关代码)。 |
| 性能考量 | 会创建一个 Result 对象,有轻微的对象创建开销,在极高性能循环中需注意。 | 几乎没有额外对象开销。 |
19、 runCatching:函数式的“结果容器”
它的核心是 Result 密封类,它有两个子类:Success(存储值)和 Failure(存储 Throwable)。
// 示例:一个可能失败的操作,使用 runCatching
val result: Result<User> = runCatching {
userRepository.fetchUser(userId) // 可能抛出 IOException 或自定义业务异常
}.onSuccess { user ->
println("获取用户成功: ${user.name}")
}.onFailure { e ->
println("操作失败: ${e.message}")
}
// 你可以后续再处理这个“结果”
when (result) {
is Result.Success -> updateUI(result.value)
is Result.Failure -> showError(result.exception)
}
优点:
- 链式调用清晰:成功与失败的回调并排,逻辑一目了然。
- 异常作为值传递:可以将“结果”传给其他函数,延迟处理。
- 安全获取值:通过
getOrNull()(失败返回null)、getOrDefault()(失败返回默认值)等安全方法取值。
20、try-catch:经典的“流程控制”
这是Java/Kotlin中最基础的异常控制结构。
// 示例:同样的操作,使用 try-catch
try {
val user = userRepository.fetchUser(userId) // 可能抛出异常
println("获取用户成功: ${user.name}")
updateUI(user) // 成功逻辑必须嵌套在try块内
} catch (e: IOException) {
println("网络错误: ${e.message}")
showError("网络连接失败")
} catch (e: BusinessException) {
println("业务错误: ${e.message}")
showError(e.message)
} finally {
// 可选的清理资源
}
优点:
- 精准捕获:可以针对不同类型的异常进行精细化处理。
- 作用域广:
try块内可以包含任意多行代码,统一捕获。 - 性能无损:没有额外的对象包装开销。