异常与错误处理

Kotlin 支持异常,但与 Java 最大的差异是:Kotlin 没有受检异常。所有异常默认都是 unchecked,你可以捕获它们,但不需要在函数签名中声明,也不会被编译器强制处理。

抛出异常

fun requirePositive(value: Int) {
    if (value <= 0) {
        throw IllegalArgumentException("value 必须大于 0")
    }
}

异常是对象,通常应该提供清晰的错误消息。需要保留原始原因时传入 cause

throw IllegalStateException("配置加载失败", cause)

预条件函数

Kotlin 标准库提供了常用检查函数:

函数 语义 失败时异常
require() 检查调用方传入的参数 IllegalArgumentException
check() 检查对象或程序状态 IllegalStateException
error() 表示不应发生的非法状态 IllegalStateException

require

fun createPage(size: Int) {
    require(size in 1..100) {
        "size 必须在 1..100 之间,实际是 $size"
    }
}

require 适合校验函数参数。调用方传错参数时,抛 IllegalArgumentException 更符合语义。

check

class Session {
    private var connected = false

    fun send(message: String) {
        check(connected) { "发送消息前必须先连接" }
        println(message)
    }
}

check 适合校验对象当前状态。状态不满足通常代表调用顺序错误或内部逻辑错误。

error

fun roleName(role: String): String =
    when (role) {
        "admin" -> "管理员"
        "viewer" -> "访客"
        else -> error("未知角色:$role")
    }

error() 的返回类型是 Nothing,因此可以放在需要任意返回类型的位置。

try-catch

try {
    val number = "42".toInt()
    println(number)
} catch (e: NumberFormatException) {
    println("不是合法数字")
}

多个 catch 时,应从具体异常到宽泛异常排列:

try {
    process()
} catch (e: IllegalArgumentException) {
    println("参数错误")
} catch (e: RuntimeException) {
    println("运行时错误")
}

try 是表达式

val number = try {
    input.toInt()
} catch (e: NumberFormatException) {
    0
}

try 分支和 catch 分支的最后一个表达式会成为整体结果。finally 总会执行,但不决定 try 表达式的结果。

finally 与资源清理

val resource = acquireResource()
try {
    resource.use()
} finally {
    resource.close()
}

JVM 上处理实现 Closeable / AutoCloseable 的资源时,更推荐标准库 use

FileWriter("output.txt").use { writer ->
    writer.write("Hello")
}

这对应 Java 的 try-with-resources 思路,但 Kotlin 用库函数表达,不需要特殊语法。

自定义异常

class UserNotFoundException(id: Long) :
    RuntimeException("找不到用户:$id")

如果希望异常可继续被继承,父异常类要显式 open

open class DomainException(message: String, cause: Throwable? = null) :
    RuntimeException(message, cause)

class OrderNotFoundException(id: Long) :
    DomainException("找不到订单:$id")

不要用 object 声明异常单例。异常带有栈追踪和上下文,每次抛出都应该创建新实例。

Nothing

Nothing 表示永远不会正常返回的表达式或函数:

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

常见用法:

val name: String = user.name ?: fail("name required")

TODO() 也返回 Nothing

fun calculate(): Int {
    TODO("尚未实现")
}

运行到 TODO() 会抛出 NotImplementedError。不要把 TODO() 留在生产路径里。

Java 互操作

Java 有 checked exception,Kotlin 没有。调用 Java 方法时,即使 Java 声明了 throws IOException,Kotlin 编译器也不会强制你捕获。

如果 Kotlin 函数要被 Java 调用,并希望 Java 调用方看到 checked exception,可以使用 @Throws

@Throws(IOException::class)
fun readConfig(): Config {
    // ...
}

这样生成的 JVM 方法签名会包含 throws IOException

安全替代函数

Kotlin 标准库经常提供 OrNull 版本来避免异常:

val number = input.toIntOrNull()
val first = list.firstOrNull()
val item = list.getOrNull(index)

如果“失败”是业务上可预期的分支,返回可空值或结果类型通常比抛异常更清晰。如果失败代表违反契约或不可恢复问题,异常更合适。

实践建议

  • 参数校验用 require,状态校验用 check
  • 异常消息要带业务上下文,例如 id、状态、输入值。
  • 可预期失败优先使用可空值、sealed result 或领域结果类型。
  • Java checked exception 迁移到 Kotlin 时,不要机械保留所有异常层级。
  • 暴露给 Java 的 Kotlin API 如需 checked exception,使用 @Throws
  • 不要吞掉异常;至少保留 cause 或日志上下文。

参考

  • 官方异常文档:https://kotlinlang.org/docs/exceptions.html