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>
类型来封装可能失败的操作结果,配合 runCatching
和 recoverCatching
可以写出更函数式的异常处理逻辑。
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