1. 概述

Java 自 7 版本起就支持在一个 catch 块中捕获多种异常,例如:

// Java code
try {
    // ...
} catch (Exception1 | Exception2 ex) {
    // Perform some common operations with ex
}

Kotlin 直到 1.7.0 版本才原生支持这一特性。因此,在本文中我们将探讨在 Kotlin 中如何实现“多异常捕获”功能。

如果你的项目还在使用较老版本的 Kotlin,或者你想了解更灵活的处理方式,下面几种方案值得参考。我们一步步来看,从最直接的方式到更具扩展性的设计。


2. 示例准备

为了演示各种解决方案,我们先构建一个示例场景。

2.1 KeyService 与自定义异常

假设我们需要实现一个 KeyService,用于存储长度为 6 的纯数字字符串密钥,并满足以下规则:

  • 密钥长度必须恰好为 6 位
  • 只能包含数字字符
  • 同一密钥不可重复存储

基于这些规则,我们定义如下四个异常类:

class KeyTooLongException(message: String = "Key-length must be six.") : Exception(message)
class KeyTooShortException(message: String = "Key-length must be six.") : Exception(message)
class InvalidKeyException(message: String = "Key should only contain digits.") : Exception(message)
class KeyAlreadyExistsException(message: String = "Key exists already.") : Exception(message)

接下来是 KeyService 的实现:

object KeyService {
    private val keyStore = mutableSetOf<String>()
    fun clearStore() = keyStore.clear()
    fun saveSixDigitsKey(digits: String) {
        when {
            digits.length < 6 -> throw KeyTooShortException()
            digits.length > 6 -> throw KeyTooLongException()
            digits.matches(Regex("""\d{6}""")).not() -> throw InvalidKeyException()
            digits in keyStore -> throw KeyAlreadyExistsException()
            else -> keyStore += digits
        }
    }
}

✅ 使用 when 表达式进行校验逻辑判断,代码清晰易读
⚠️ 注意:这里将 KeyService 设计为 object(单例),便于测试时统一管理状态

2.2 SaveKeyResult 枚举

为了统一返回结果,我们创建一个枚举表示保存操作的结果:

enum class SaveKeyResult {
    SUCCESS, FAILED, SKIPPED_EXISTED_KEY
}

含义如下:

  • SUCCESS:密钥合法且成功保存
  • FAILED:因长度、格式等问题导致失败(对应前三种异常)
  • SKIPPED_EXISTED_KEY:密钥已存在,跳过保存

后续我们会实现多个 saveX() 方法来对比不同异常处理策略。每个方法都会调用 KeyService.saveSixDigitsKey() 并返回对应的 SaveKeyResult

测试前清空存储:

@BeforeEach
fun cleanup() {
    KeyService.clearStore()
}

3. 多个 catch 块(基础写法)

最直观的做法就是为每种异常单独写一个 catch 块:

fun save1(theKey: String): SaveKeyResult {
    return try {
        KeyService.saveSixDigitsKey(theKey)
        SUCCESS
    } catch (ex: KeyTooShortException) {
        FAILED
    } catch (ex: KeyTooLongException) {
        FAILED
    } catch (ex: InvalidKeyException) {
        FAILED
    } catch (ex: KeyAlreadyExistsException) {
        SKIPPED_EXISTED_KEY
    }
}

测试用例覆盖所有情况:

assertEquals(FAILED, save1("42"))
assertEquals(FAILED, save1("1234567"))
assertEquals(FAILED, save1("kotlin"))
assertEquals(SUCCESS, save1("123456"))
assertEquals(SKIPPED_EXISTED_KEY, save1("123456"))

✅ 优点:逻辑清晰,适合异常处理逻辑不同的场景
❌ 缺点:重复代码多,维护成本高 —— 尤其当多个异常需要相同处理时

💡 踩坑提醒:这种写法虽然简单,但在实际项目中容易造成 catch 块膨胀,建议仅用于异常处理差异较大的场景。


4. 在 catch 块中使用 when 判断类型

为了减少重复代码,可以只用一个 catch(Exception) 捕获父类异常,再通过 when 分支判断具体类型:

fun save2(theKey: String): SaveKeyResult {
    return try {
        KeyService.saveSixDigitsKey(theKey)
        SUCCESS
    } catch (ex: Exception) {
        when (ex) {
            is KeyTooLongException,
            is KeyTooShortException,
            is InvalidKeyException -> FAILED
            is KeyAlreadyExistsException -> SKIPPED_EXISTED_KEY
            else -> throw ex
        }
    }
}

同样使用上述测试用例验证,结果通过。

✅ 优点:

  • 减少了 catch 块数量
  • 支持对一组异常统一处理
  • 写法简洁,易于理解

⚠️ 注意事项:

  • 必须加 else -> throw ex,否则会吞掉未知异常(这是大忌!)
  • 若未来新增异常类型而未更新 when,可能导致意外行为

💡 提示:这种方式在 Kotlin 中非常常见,属于“约定优于配置”的典型实践。


5. 创建 multiCatch 扩展函数

Kotlin 的扩展函数特性让我们可以封装通用逻辑。我们可以尝试实现一个类似 Java 多 catch 的 multiCatch 函数。

思路如下:

try 中的执行体看作一个无参函数 () -> R,然后为其添加扩展:

inline fun <R> (() -> R).multiCatch(
    vararg exceptions: KClass<out Throwable>,
    thenDo: () -> R
): R {
    return try {
        this()
    } catch (ex: Exception) {
        if (ex::class in exceptions) thenDo() else throw ex
    }
}

📌 参数说明:

  • exceptions: 可变参数,传入要捕获的异常类型
  • thenDo: 发生指定异常时执行的替代逻辑

使用方式:

fun save3(theKey: String): SaveKeyResult {
    return try {
        {
            KeyService.saveSixDigitsKey(theKey)
            SUCCESS
        }.multiCatch(
            KeyTooShortException::class,
            KeyTooLongException::class,
            InvalidKeyException::class
        ) { FAILED }
    } catch (ex: KeyAlreadyExistsException) {
        SKIPPED_EXISTED_KEY
    }
}

测试验证:

assertEquals(FAILED, save3("42"))
assertEquals(FAILED, save3("1234567"))
assertEquals(FAILED, save3("kotlin"))
assertEquals(SUCCESS, save3("123456"))
assertEquals(SKIPPED_EXISTED_KEY, save3("123456"))

✅ 优点:

  • 封装了“多异常 → 统一处理”的模式
  • 命名语义明确,提升可读性

❌ 缺点:

  • 返回的是结果值 R 而非原函数,无法链式处理多组异常
  • 仍需外层 try-catch 处理其他异常类型

⚠️ 注意:由于 multiCatch 是 inline 函数,性能较好,但不要滥用,避免生成过多内联代码。


6. 扩展 Result 类实现优雅异常处理

Kotlin 提供了 Result<T> 类型来封装可能失败的操作结果,配合 runCatchingrecoverCatching 可以写出更函数式的异常处理逻辑。

6.1 runCatching 与 Result 简介

runCatching 可以安全执行一段可能抛异常的代码,返回 Result<T>

runCatching {
    KeyService.saveSixDigitsKey(theKey)
    SUCCESS
} // 返回 Result<SaveKeyResult>
  • 成功时:isSuccess == true,可通过 getOrThrow() 获取值
  • 失败时:isFailure == true,内部封装了异常

利用 recoverCatching 可以将异常转换为默认值:

result.recoverCatching { ex -> 
    if (ex is SomeException) DEFAULT_VALUE else throw ex 
}

6.2 自定义 onException 扩展

我们基于 recoverCatching 实现更易用的 onException 扩展:

inline fun <R, T : R> Result<T>.onException(
    vararg exceptions: KClass<out Throwable>,
    transform: (exception: Throwable) -> T
) = recoverCatching { ex ->
    if (ex::class in exceptions) {
        transform(ex)
    } else throw ex
}

使用该扩展重构 save 方法:

fun save4(theKey: String): SaveKeyResult {
    return runCatching {
        KeyService.saveSixDigitsKey(theKey)
        SUCCESS
    }.onException(
        KeyTooShortException::class,
        KeyTooLongException::class,
        InvalidKeyException::class
    ) {
        FAILED
    }.onException(KeyAlreadyExistsException::class) {
        SKIPPED_EXISTED_KEY
    }.getOrThrow()
}

测试用例依然全部通过。

✅ 优势非常明显:

  • 完全消除 try-catch 嵌套,代码扁平化
  • 支持链式调用,轻松处理多组“异常 → 结果”映射
  • 更符合函数式编程风格,逻辑表达力强

📌 推荐在复杂业务逻辑或异步流程中使用此模式。


7. 总结

本文介绍了四种在 Kotlin 中实现“多异常捕获”的方式:

方案 是否推荐 适用场景
多个 catch 块 异常处理逻辑完全不同
when + 单 catch 简单项目或轻量级处理
multiCatch 扩展 ⚠️ 需要复用但结构不复杂
Result + onException ✅✅✅ 复杂逻辑、高可维护性需求

📌 最佳实践建议:

  • 日常开发优先使用 runCatching {} .onException(...) 链式写法
  • 避免吞异常,尤其是 else -> throw ex 不可省略
  • 对于公共组件,考虑封装成统一的异常处理器

所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-5


原始标题:Catch Multiple Exceptions in Kotlin