异常处理:try-catch-finally、Nothing类型、自定义异常、Kotlin的受检异常处理

异常处理,说白了就是给程序穿上防弹衣。你写的代码不可能永远一帆风顺,网络会断、文件会丢、用户会乱点。我做了这么多年项目,最怕的就是那种「静默失败」——程序没崩溃,但结果全错了,你还不知道错在哪。

Kotlin 的异常处理,跟 Java 比其实更简洁。但有几个关键点,你得搞明白。

try-catch-finally:基本功,但别滥用

先看最基础的写法。Kotlin 的 try-catch 可以当表达式用,这是 Java 没有的。

val result = try {
    riskyOperation()
} catch (e: IOException) {
    println("网络出问题了: ${e.message}")
    "默认值"  // catch 块最后一行就是返回值
} finally {
    println("不管怎样,我都会执行")
    // 注意:finally 里不要写 return,会覆盖上面的返回值
}

我个人习惯把 try-catch 用在真正不可控的地方。比如解析用户输入、读写文件、网络请求。但像空指针这种,我建议用 Kotlin 的 null 安全机制去处理,别用 try-catch 兜底。

小技巧: try-catch 作为表达式时,catch 块和 try 块最后一行类型必须一致,或者有共同的父类型。否则编译器会报错。

我在项目中遇到过一个问题:有个同事在 finally 里写了 return,结果 catch 块里的返回值全被覆盖了。排查了半天才发现。嗯,这里要注意——finally 里别写 return,除非你故意想搞事情。

Nothing 类型:Kotlin 的「不可能」类型

Nothing 是 Kotlin 里一个很有意思的类型。它表示「永远不会有返回值」。你想想看,什么情况下一个函数没有返回值?

  • 函数抛异常了
  • 函数进入死循环了
  • 函数调用了 exitProcess() 直接退出程序

Kotlin 用 Nothing 类型来标记这种情况。比如标准库里的 TODO() 函数,返回值就是 Nothing。

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

fun calculate(x: Int): Int {
    if (x < 0) {
        fail("x 不能为负数")  // 这里返回 Nothing,编译器知道后面不会执行
    }
    return x * 2
}

为什么要有 Nothing?说白了,它让类型系统更严谨。编译器知道 fail() 永远不会正常返回,所以 calculate() 里 if 后面的代码,编译器能确定只有 x >= 0 时才会执行。这能帮你避免一些潜在的空指针问题。

核心理解: Nothing 是所有类型的子类型。你可以把它赋值给任何变量,或者作为任何函数的返回值。但你不能创建 Nothing 的实例——因为它根本不存在。

自定义异常:别偷懒,取个好名字

Java 里自定义异常很简单,Kotlin 也一样。但我见过太多人直接抛 Exception("出错了")。你想想看,这种异常抛出来,调用方怎么处理?只能 catch (e: Exception) 一把抓,然后打印日志完事。

我建议你给异常取个有业务含义的名字。比如用户余额不足,就抛 InsufficientBalanceException;文件格式不对,就抛 InvalidFileFormatException。

class InsufficientBalanceException(
    val currentBalance: Double,
    val requiredAmount: Double
) : Exception("余额不足,当前 $currentBalance,需要 $requiredAmount")

fun withdraw(amount: Double) {
    val balance = getBalance()
    if (amount > balance) {
        throw InsufficientBalanceException(balance, amount)
    }
    // 正常扣款逻辑
}

这样做的好处是,调用方可以精确捕获并处理。比如前端可以弹窗提示「您还差 xxx 元」,而不是显示一个笼统的「系统错误」。

避坑指南: 自定义异常类最好包含足够的上下文信息。我曾经接手过一个项目,异常只传了个字符串,排查问题时根本不知道当时的数据状态。后来我改成了带字段的异常类,调试效率翻倍。

Kotlin 的受检异常处理:Java 的痛点,Kotlin 的解放

Java 的受检异常(Checked Exception)是个争议很大的设计。你写个文件读取,就得 try-catch 或者 throws。代码里全是 try-catch 嵌套,看着就头疼。

Kotlin 直接取消了受检异常。所有异常都是非受检的。这意味着你不需要在函数签名上声明 throws,也不需要强制捕获。

// Java 写法
public void readFile() throws IOException {
    // ...
}

// Kotlin 写法
fun readFile() {
    // 不需要 throws,也不需要强制 try-catch
}

有人可能会担心:这样会不会导致异常被忽略?其实不会。Kotlin 鼓励你用更优雅的方式处理异常:

  • 用 Result 类型包装可能失败的操作
  • 用 runCatching 替代 try-catch
  • 用 sealed class 表示成功/失败状态
// 使用 runCatching
val result = runCatching {
    riskyOperation()
}

result.onSuccess { value ->
    println("操作成功: $value")
}.onFailure { exception ->
    println("操作失败: ${exception.message}")
}

// 或者用 Result 类型
fun fetchData(): Result<String> = runCatching {
    // 网络请求
    "数据"
}

我个人觉得,Kotlin 这个设计是对的。受检异常在大型项目里反而容易导致反模式——很多人直接在 catch 里写空实现,或者 throws Exception 一路往上抛。与其这样,不如让开发者自己决定怎么处理。

我的建议: 如果你在写库或者框架,可以考虑用 sealed class 定义错误类型。这样调用方可以用 when 表达式穷举所有错误情况,比 try-catch 更安全。

知识体系总览

下面这张图帮你理清 Kotlin 异常处理的核心脉络。从基础语法到高级用法,一目了然。

Kotlin 异常处理知识体系 异常处理 try-catch-finally Nothing 类型 自定义异常 受检异常处理 表达式用法 finally 陷阱 类型系统 TODO() 业务语义 上下文信息 runCatching Result 类型

总结一下:Kotlin 的异常处理,核心就四个点。try-catch-finally 是基本功,Nothing 类型是 Kotlin 的特色,自定义异常体现你的设计能力,受检异常的处理方式则展示了 Kotlin 的务实态度。把这些搞明白,你写出来的代码会健壮很多。

一句话记住: 异常处理不是为了消灭异常,而是为了在异常发生时,程序还能保持可控。

公众号:蓝海资料掘金营,微信deep3321