2026年面试3

42 阅读12分钟

视若飞

1、Kotlin中延迟方法有多少种?

在 Kotlin 中实现“延迟”有多种方式,根据其设计目标和应用场景,可以系统地分为以下几类。你可以通过下图快速了解它们的核心区别与关联:

deepseek_mermaid_20260121_49c9b4.png

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

这是协程的一种启动模式,用于控制协程的执行时机

  • 特点:使用 launchasync 创建协程时不会立即执行,直到手动调用 start()/join() (对 Job) 或 await() (对 Deferred)。
  • 使用场景:需要定义但不立即执行协程,或构建复杂的协程依赖关系。
val lazyJob = launch(start = CoroutineStart.LAZY) {
    println("这个协程不会立即运行")
}
// ... 在某个时刻
lazyJob.start() // 此时协程才开始执行

6、异步结果的延迟获取:asyncDeferred

这是结构化并发中用于并发执行并延迟获取结果的模式。

  • 特点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是挂起)并发执行任务,最后聚合结果。
面试回答要点

当被问到这个问题时,不要只列举方法,而要体现分类思维

  1. 首先区分维度:“延迟”在Kotlin中主要有三个维度:时间的延迟sleep/delay)、初始化的延迟lazy)、执行的延迟LAZY启动模式)。
  2. 强调协程首选:在协程中,时间延迟绝对应该使用 delay() 而非 Thread.sleep() ,因为前者是协作式的,不阻塞线程。
  3. 澄清易混概念:特别说明 lazy属性委托,用于初始化;而 CoroutineStart.LAZY启动选项,用于控制协程执行时机。两者名字相似但用途完全不同。
  4. 提及高级模式:可以补充 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() (有结果等待)
构建器launchasync
返回对象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("全部结束")
}
常见误区
  1. async 但不调用 await:如果你用 async 启动协程却从不调用 await,你就无法获取结果,也无法感知任务中的异常(因为异常会存储在 Deferred 对象中,直到 await 被调用时才抛出)。所以对于“没有结果”的任务,请直接用 launch
  2. launch 块中返回无意义值:不要这样写:async { doWork(); Unit }.await(),这浪费了 async 创建 Deferred 对象的开销,直接用 launch { doWork() }.join() 更清晰高效。
当被问到与 async/await 的对比时,可以这样回答:

“对于不需要返回结果的并发任务,Kotlin 协程使用 launch 构建器来启动,它返回一个 Job 对象。通过调用这个 Jobjoin() 挂起函数,我们可以等待该任务完成。这与 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()
                )
            }
        }
    }
}

这种封装遵循“关注点分离”原则,将以下通用逻辑从业务代码中抽离:

  1. 协程生命周期管理:自动在 viewModelScope 中启动。
  2. 加载状态管理:统一处理加载对话框的显示与隐藏。
  3. 异常处理与转换:将各种异常(网络、解析、业务)转换为统一的 AppException 传递给UI层。
  4. 响应体校验:统一检查服务器返回的业务状态码。

我们来拆解这个 request 函数的每个部分:

代码部分设计目的与原理带来的好处
fun <T> BaseViewModel.request(...): Job1. 泛型 <T> :支持任意类型的响应数据。 2. 定义在 BaseViewModel:所有子ViewModel都可复用。 3. 返回 Job:允许调用者在需要时取消请求。类型安全、代码复用、可控性
block: suspend () -> BaseResponse<T>核心的请求执行体。是一个挂起函数,由调用者传入具体的请求(如 repository.getUser())。解耦:封装器不关心具体请求,只负责执行和包装流程。
success: (T) -> Unit成功回调。接收从 BaseResponse 中提取的真正的业务数据 T业务层只关注最终有用的数据,无需处理响应包装。
error: (AppException) -> Unit失败回调。接收经过统一处理的、面向UI的异常。UI层得到友好、统一的错误信息,可直接用于提示。
isShowDialogloadingChange通过 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层只需处理一种异常类型。
优势总结
  1. 高度复用,代码精简:业务ViewModel代码极简,只需一行 request 调用。
  2. 统一管理,行为一致:所有网络请求的加载、错误处理方式一致。
  3. 安全可靠:依托 viewModelScope,自动避免生命周期问题。
  4. 易于测试block 参数易于用模拟对象替换,方便单元测试。

12、Kotlin 协程怎么处理异常的?

deepseek_mermaid_20260121_981604.png

核心规则与机制
  1. 异常会传播:协程中未捕获的异常会向其父协程传播,导致父协程取消,进而取消其所有子协程(这是结构化并发取消的一部分)。

  2. 两种传播方式

    • launch 构建器:异常会立即在发生处抛出,并开始传播。
    • async 构建器:异常会延迟到您调用 .await() 获取结果时才抛出。

13、Kotlin协程异常的四大处理方式与代码示例

方式一:直接 try-catch(最基础)

直接在可能发生异常的代码块周围使用 try-catch。这对于 launchasync 都有效。

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、协程的异常处理回答

当被问到时,可以这样结构化回答:

  1. 先讲核心哲学:“协程的异常处理遵循结构化并发原则,未处理异常会向上传播并取消整个协程作用域,这保证了程序的健壮性。”

  2. 分两点阐述传播差异

    • launch 启动的协程,异常会立即传播,通常需要用 try-catchCoroutineExceptionHandler 在根协程捕获。”
    • async 启动的协程,异常会延迟到调用 await() 时才抛出,必须在 await() 处处理,更好的做法是在 async 块内部封装成 Result 类返回。”
  3. 强调关键工具 SupervisorJob:“为了防止一个子任务失败导致整个作用域取消,我们需要使用 SupervisorJobsupervisorScope,这在处理并行的、独立的任务时(如同时上传多张图片)至关重要。”

  4. 给出最佳实践:“在生产代码中,我们通常会:

    • 为根协程作用域配置 CoroutineExceptionHandler 来兜底未捕获异常,并上报日志。
    • 对于并行的独立任务,使用 supervisorScope
    • 对于需要结果的异步计算,使用 async 并在内部捕获异常,返回 Result 密封类。
    • 始终记得,在 Android 的 viewModelScopelifecycleScope 中,它们默认使用了 SupervisorJob,所以一个屏幕内的多个协程失败不会相互影响。”

15、MMKV 内部是怎么实现的?

1. 基石:内存映射 (mmap)
  • 是什么:MMKV在初始化时,通过系统调用将磁盘上的一个文件直接映射到进程的虚拟内存地址空间中

  • 操作文件就像操作数组一样。

  • 关键优势

    • 高性能读写:数据直接位于内存,省去了传统I/O在用户态与内核态之间的数据拷贝开销,写入速度基本等同于内存操作
    • 数据安全:写入内存的数据由操作系统内核在适当时机(如内存压力、进程退出)自动回写到磁盘,进程崩溃也不会丢失数据
2. 高效序列化:Protocol Buffer (protobuf)

MMKV使用protobuf对键值对进行二进制编码 。相比SharedPreferences的XML格式,protobuf编码更紧凑、序列化/反序列化速度更快,这也是MMKV高性能和节省空间的重要原因。

3. 核心设计:Append-Only(追加写)与增量更新

这是MMKV区别于SharedPreferences全量更新的核心。

  • 写入过程:每次写入(无论新增、修改或删除),MMKV并不是就地修改旧数据,而是将新的键值对直接以protobuf格式追加到内存末尾
  • 这确保了每次写入都是顺序、单次I/O操作,极其高效。
  • 索引维护:为了能快速定位数据,MMKV在内存中维护了一个哈希表,记录每个Key对应数据在内存映射区域中的起始位置和大小。每次追加新数据后,这个内存索引会同步更新。
4. 空间回收:文件重整与扩容

“只追加不删除”的模式会导致文件不断增大,并产生大量过期数据。MMKV通过以下机制解决:

  • 文件重整:当文件大小超出预期或过期数据太多时,MMKV会进行一次重整。遍历有效数据,将其重新紧凑地写入一个新文件(或原文件的前部),然后替换旧文件。这个过程去除了所有冗余数据。
  • 动态扩容:当可用空间不足且重整后仍无法满足时,MMKV会进行扩容(通常是将文件扩大一倍),以满足后续写入。
5. 多进程与线程安全
  • 多进程支持:MMKV通过文件锁fcntl)或命名信号量来实现跨进程同步,确保多个进程同时读写数据的一致性

  • 线程安全:内部使用读写锁自旋锁来保证多线程并发安全,不同线程的读写操作互不影响

总结:MMKV的三大优势

从原理上看,MMKV的优势来源于

  1. mmap技术:保证了I/O的高性能和崩溃数据不丢失
  2. protobuf编码:实现了高效序列化和紧凑存储
  3. 追加写与增量更新:彻底避免了全量写入,让每次写入都轻量而快速

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做了哪些优化?”时,可以按以下逻辑回答:

  1. 核心防ANR优化:强调 apply() 机制,说明其异步写入如何避免主线程卡顿。
  2. 性能优化:提及异步加载、内存缓存、commit合并,减少I/O和等待。
  3. 数据安全优化:说明原子写入和崩溃恢复机制如何保证数据完整性。
  4. 指出局限性:说明其仍存在的全量更新、不适合大量数据、多进程不可靠等问题,并引出 DataStoreMMKV 等现代替代方案。

实际开发建议

  • 场景:仅用于存储少量、非关键、低频更新的简单配置(如用户设置开关、界面主题)。
  • 禁忌:不要存储JSON字符串、大字符串、频繁更新的计数器、敏感信息
  • 方法始终使用apply() ,除非你必须立即知道写入结果(这种情况极少)。
  • 替代:对于复杂数据、高性能或跨进程需求,考虑使用 DataStore (官方推荐)、MMKVSQLite

总之,SharedPreferences的优化使其在特定场景下更安全可用,但其架构限制决定了它并非现代数据存储的最佳选择。

18、runCatching { ... } 和try-catch的区别?

runCatching { ... } 和传统的 try-catch 都是为了处理异常,但它们的核心思想、代码风格和适用场景有根本区别。简单来说:runCatching“函数式” 的风格,把异常封装成一个可传递的结果;而 try-catch“命令式” 的风格,立即中断流程并进行处理

为了方便你快速理解核心区别,我将它们的主要差异总结在下表中:

维度runCatching { ... }传统 try-catch
核心机制函数式、声明式。将执行块和异常都封装Result 对象中。命令式、过程式。直接中断流程并跳转到 catch
返回值始终返回一个 Result<T> 对象(成功包含值,失败包含异常)。没有固定返回值,依赖在块内赋值或直接处理。
代码风格支持链式调用.onSuccess.onFailure.map等),流程清晰。代码呈块状结构,成功与失败逻辑被物理分隔。
异常处理时机延迟处理。异常被安全存储,你可以在链式调用的最后决定何时、如何处理立即处理。异常抛出后必须立即在 catch 块中处理。
组合能力强。可以轻松地与 maprecover 等其他函数式操作符组合,进行转换或恢复。弱。难以直接与其他操作组合,嵌套复杂逻辑时代码易乱。
作用范围仅作用于其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 块内可以包含任意多行代码,统一捕获。
  • 性能无损:没有额外的对象包装开销。