Kotlin 异常处理
异常处理是保证程序健壮性的重要机制。Kotlin 的异常处理设计简洁而现代,所有异常都是非受检的(unchecked),这意味着你不必强制捕获或声明异常。本章将详细介绍如何抛出异常、捕获异常、自定义异常以及最佳实践。
异常基础概念
什么是异常?
异常是程序运行时发生的意外事件,它会中断正常的程序执行流程。Kotlin 使用异常来处理错误情况,让代码能够优雅地处理意外状况而不是直接崩溃。
异常层次结构
Kotlin 的异常层次结构以 Throwable 为根:
Throwable
├── Error(严重错误,通常不应捕获)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception(可处理的异常)
├── RuntimeException(运行时异常)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IndexOutOfBoundsException
│ └── ...
├── IOException(IO 异常)
└── ...
Error vs Exception 的区别:
Error:表示严重的系统级错误,应用程序通常无法恢复,如内存耗尽、栈溢出Exception:表示程序可以尝试处理的异常情况
受检与非受检异常
与 Java 不同,Kotlin 不区分受检异常(checked exception)和非受检异常(unchecked exception):
// Kotlin:不需要声明抛出的异常
fun readFile(path: String): String {
// 可能抛出 IOException,但不需要在签名中声明
return java.io.File(path).readText()
}
// Java 等效代码需要声明:
// public String readFile(String path) throws IOException { ... }
这种设计简化了代码,减少了不必要的 try-catch 嵗套,同时保持了灵活性——你仍然可以选择捕获异常。
抛出异常
throw 关键字
使用 throw 关键字手动抛出异常:
fun main() {
fun divide(a: Int, b: Int): Int {
if (b == 0) {
throw ArithmeticException("除数不能为零")
}
return a / b
}
println(divide(10, 2)) // 5
// println(divide(10, 0)) // 抛出异常
}
异常对象
抛出异常时,可以提供详细信息:
fun main() {
fun setAge(age: Int) {
if (age < 0) {
// 提供错误消息
throw IllegalArgumentException("年龄不能为负数: $age")
}
if (age > 150) {
// 提供错误消息和原因
throw IllegalArgumentException(
"年龄不合理: $age",
RuntimeException("年龄超出合理范围")
)
}
println("年龄设置为: $age")
}
setAge(25) // 年龄设置为: 25
// setAge(-5) // IllegalArgumentException: 年龄不能为负数: -5
}
Nothing 类型
throw 表达式的类型是 Nothing,这是一个特殊的类型,表示"永远不会返回":
// Nothing 是所有类型的子类型
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
fun main() {
// 编译器知道 fail 之后代码不会执行
val name: String = null ?: fail("姓名不能为空")
println(name.length) // 安全访问,编译器知道 name 不为 null
// 在 when 表达式中使用
val result = when (val x = (1..10).random()) {
in 1..5 -> "小"
in 6..10 -> "大"
else -> fail("不可能的情况") // 编译器知道这里不会返回
}
println(result)
}
Nothing 的实际应用:
fun main() {
// TODO() 函数返回 Nothing
fun calculate(x: Int): Int = TODO("等待实现")
// 编译器警告:后面的代码不可达
// val y = calculate(5)
// 在递归函数中标记永不返回
tailrec fun infiniteLoop(): Nothing {
// 执行某些操作
infiniteLoop()
}
}
捕获异常
try-catch 基本语法
使用 try-catch 块捕获并处理异常:
fun main() {
fun parseInt(str: String): Int? {
return try {
str.toInt()
} catch (e: NumberFormatException) {
println("无法解析 '$str' 为整数")
null
}
}
println(parseInt("123")) // 123
println(parseInt("abc")) // null(打印:无法解析 'abc' 为整数)
}
try 作为表达式
try 可以作为表达式使用,返回最后一个表达式的值:
fun main() {
val result = try {
"100".toInt()
} catch (e: NumberFormatException) {
-1 // 解析失败返回 -1
}
println(result) // 100
// 更复杂的例子
fun safeDivide(a: Int, b: Int): String {
return try {
val result = a / b
"结果: $result"
} catch (e: ArithmeticException) {
"错误: ${e.message}"
}
}
println(safeDivide(10, 2)) // 结果: 5
println(safeDivide(10, 0)) // 错误: / by zero
}
多个 catch 块
一个 try 块可以有多个 catch 块,按从具体到一般的顺序排列:
fun main() {
fun process(input: String) {
try {
val number = input.toInt()
val result = 100 / number
println("结果: $result")
} catch (e: NumberFormatException) {
println("输入不是有效数字: $input")
} catch (e: ArithmeticException) {
println("数学错误: ${e.message}")
} catch (e: Exception) {
println("未知错误: ${e::class.simpleName}")
}
}
process("5") // 结果: 20
process("abc") // 输入不是有效数字: abc
process("0") // 数学错误: / by zero
}
重要原则:catch 块的顺序很重要,更具体的异常类型应该放在前面。因为 JVM 会按顺序匹配,一旦匹配成功就不会继续检查后面的 catch 块。
finally 块
finally 块中的代码无论是否发生异常都会执行,通常用于资源清理:
fun main() {
fun readFileContent(path: String): String? {
var reader: java.io.BufferedReader? = null
try {
reader = java.io.File(path).bufferedReader()
return reader.readText()
} catch (e: java.io.IOException) {
println("读取文件失败: ${e.message}")
return null
} finally {
// 无论成功或失败,都会执行
println("清理资源...")
reader?.close()
}
}
// 使用 try 表达式 + finally
fun calculate(a: Int, b: Int): Int {
return try {
a / b
} finally {
println("计算完成") // 总是执行
}
}
println(calculate(10, 2))
// 输出:
// 计算完成
// 5
// println(calculate(10, 0))
// 输出:
// 计算完成
// 然后抛出 ArithmeticException
}
finally 与返回值:finally 块不会改变 try-catch 的返回值:
fun main() {
fun test(): Int {
try {
return 1
} finally {
// 这行不影响返回值
println("finally 执行")
}
}
println(test()) // 先打印 "finally 执行",再打印 1
// 注意:在 finally 中使用 return 是不推荐的
fun badPractice(): Int {
try {
return 1
} finally {
return 2 // 会覆盖 try 中的返回值!
}
}
println(badPractice()) // 2
}
只有 finally 的 try
如果不需要处理异常,只需要清理资源,可以只用 try-finally:
fun main() {
fun processData(data: List<Int>) {
println("开始处理...")
try {
// 执行可能出错的操作
data.forEach {
if (it < 0) throw IllegalArgumentException("负数")
println(it)
}
} finally {
println("清理资源")
}
}
// 异常会向上传播,但 finally 仍会执行
try {
processData(listOf(1, 2, -1, 3))
} catch (e: IllegalArgumentException) {
println("捕获异常: ${e.message}")
}
// 输出:
// 开始处理...
// 1
// 2
// 清理资源
// 捕获异常: 负数
}
预条件函数
Kotlin 提供了几个内置函数用于抛出特定类型的异常,这些函数简化了常见的参数和状态检查。
require() - 参数检查
require() 用于验证函数参数,失败时抛出 IllegalArgumentException:
fun main() {
fun setAge(age: Int) {
require(age >= 0) { "年龄不能为负数" }
require(age <= 150) { "年龄不合理" }
println("年龄: $age")
}
fun createUser(name: String, email: String) {
require(name.isNotBlank()) { "姓名不能为空" }
require(email.contains("@")) { "邮箱格式不正确" }
println("创建用户: $name ($email)")
}
fun findUser(id: Int): String {
require(id > 0) { "ID 必须为正数" }
return "User$id"
}
setAge(25) // 年龄: 25
createUser("张三", "[email protected]")
// setAge(-1) // IllegalArgumentException: 年龄不能为负数
// createUser("", "test") // IllegalArgumentException: 姓名不能为空
}
check() - 状态检查
check() 用于验证对象状态,失败时抛出 IllegalStateException:
class Connection {
private var connected = false
fun connect() {
connected = true
println("已连接")
}
fun disconnect() {
connected = false
println("已断开")
}
fun send(data: String) {
check(connected) { "连接未建立,无法发送数据" }
println("发送: $data")
}
fun receive(): String {
check(connected) { "连接未建立,无法接收数据" }
return "响应数据"
}
}
fun main() {
val conn = Connection()
// conn.send("hello") // IllegalStateException: 连接未建立,无法发送数据
conn.connect()
conn.send("hello") // 发送: hello
}
requireNotNull() - 非空检查
requireNotNull() 确保值不为 null,否则抛出 IllegalArgumentException:
fun main() {
fun processName(name: String?) {
val nonNullName = requireNotNull(name) { "姓名不能为空" }
println("处理: $nonNullName")
}
processName("张三") // 处理: 张三
// processName(null) // IllegalArgumentException: 姓名不能为空
// 智能类型转换
fun getStringLength(str: String?): Int {
val s = requireNotNull(str) { "字符串为空" }
// s 被智能转换为非空 String
return s.length
}
}
checkNotNull() - 非空状态检查
checkNotNull() 确保值不为 null,否则抛出 IllegalStateException:
class Service {
private var data: String? = null
fun loadData() {
data = "已加载数据"
}
fun process() {
val d = checkNotNull(data) { "数据尚未加载" }
println("处理: $d")
}
}
fun main() {
val service = Service()
// service.process() // IllegalStateException: 数据尚未加载
service.loadData()
service.process() // 处理: 已加载数据
}
error() - 错误状态
error() 用于表示不应该发生的情况,直接抛出 IllegalStateException:
fun main() {
enum class Role { ADMIN, USER, GUEST }
fun getPermissions(role: Role): List<String> = when (role) {
Role.ADMIN -> listOf("read", "write", "delete")
Role.USER -> listOf("read", "write")
Role.GUEST -> listOf("read")
}
// 在 when 表达式中处理不应发生的情况
fun processRole(role: Role?): String = when (role) {
Role.ADMIN -> "管理员"
Role.USER -> "用户"
Role.GUEST -> "访客"
null -> error("角色不能为空")
}
println(processRole(Role.ADMIN)) // 管理员
// println(processRole(null)) // IllegalStateException: 角色不能为空
}
预条件函数对比
| 函数 | 抛出异常 | 使用场景 |
|---|---|---|
require() | IllegalArgumentException | 验证参数 |
requireNotNull() | IllegalArgumentException | 确保参数非空 |
check() | IllegalStateException | 验证状态 |
checkNotNull() | IllegalStateException | 确保状态非空 |
error() | IllegalStateException | 表示不应发生的情况 |
自定义异常
创建自定义异常
通过继承 Exception 或其子类创建自定义异常:
// 基本自定义异常
class ValidationException(message: String) : Exception(message)
// 更完整的自定义异常
class BusinessException(
message: String,
cause: Throwable? = null
) : Exception(message, cause) {
constructor(message: String) : this(message, null)
}
// 继承特定异常类型
class InsufficientFundsException(
val required: Double,
val available: Double
) : IllegalStateException(
"资金不足:需要 $required,可用 $available"
)
fun main() {
fun withdraw(balance: Double, amount: Double): Double {
if (amount > balance) {
throw InsufficientFundsException(amount, balance)
}
return balance - amount
}
println(withdraw(100.0, 50.0)) // 50.0
// println(withdraw(100.0, 150.0))
// InsufficientFundsException: 资金不足:需要 150.0,可用 100.0
}
异常层次结构
为复杂应用设计异常层次结构:
// 抽象基类
sealed class AppException(
message: String,
cause: Throwable? = null
) : Exception(message, cause) {
// 用户相关异常
class UserNotFoundException(val userId: Long) :
AppException("用户不存在: $userId")
class InvalidCredentialsException(message: String = "用户名或密码错误") :
AppException(message)
// 支付相关异常
class PaymentException(
message: String,
cause: Throwable? = null
) : AppException(message, cause)
class InsufficientBalanceException(
val required: Double,
val available: Double
) : PaymentException("余额不足:需要 $required,实际 $available")
class PaymentTimeoutException(
val transactionId: String
) : PaymentException("支付超时: $transactionId")
}
fun main() {
fun processPayment(userId: Long, amount: Double) {
// 模拟检查用户
if (userId < 0) {
throw AppException.UserNotFoundException(userId)
}
// 模拟检查余额
val balance = 100.0
if (amount > balance) {
throw AppException.InsufficientBalanceException(amount, balance)
}
println("支付成功")
}
try {
processPayment(-1, 50.0)
} catch (e: AppException.UserNotFoundException) {
println("用户错误: ${e.message}")
} catch (e: AppException.PaymentException) {
println("支付错误: ${e.message}")
} catch (e: AppException) {
println("应用错误: ${e.message}")
}
}
使用密封类组织异常
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
}
sealed class AppError {
abstract val message: String
data class NetworkError(override val message: String, val code: Int) : AppError()
data class ValidationError(override val message: String, val field: String) : AppError()
data class AuthError(override val message: String) : AppError()
data class UnknownError(override val message: String, val cause: Throwable?) : AppError()
}
fun main() {
fun fetchUser(id: Long): Result<String> {
return if (id > 0) {
Result.Success("User$id")
} else {
Result.Failure(AppError.ValidationError("ID 必须为正数", "id"))
}
}
when (val result = fetchUser(-1)) {
is Result.Success -> println("成功: ${result.data}")
is Result.Failure -> when (result.error) {
is AppError.NetworkError -> println("网络错误: ${result.error.message}")
is AppError.ValidationError -> println("验证失败: ${result.error.field} - ${result.error.message}")
is AppError.AuthError -> println("认证错误: ${result.error.message}")
is AppError.UnknownError -> println("未知错误: ${result.error.message}")
}
}
}
异常最佳实践
1. 使用特定的异常类型
// 推荐:使用具体的异常类型
fun setAge(age: Int) {
require(age >= 0) { "年龄不能为负数" }
require(age <= 150) { "年龄不合理" }
}
// 不推荐:使用通用异常
fun setAgeBad(age: Int) {
if (age < 0 || age > 150) {
throw Exception("年龄无效") // 太通用
}
}
2. 提供有意义的错误信息
// 推荐:详细的错误信息
fun transfer(from: String, to: String, amount: Double) {
require(amount > 0) {
"转账金额必须为正数,当前: $amount"
}
require(from != to) {
"转出账户和转入账户不能相同: $from"
}
}
// 不推荐:无意义的错误信息
fun transferBad(from: String, to: String, amount: Double) {
require(amount > 0) { "error" }
}
3. 正确处理资源
// 推荐:使用 use 函数自动关闭资源
fun readFile(path: String): String {
return java.io.File(path).bufferedReader().use {
it.readText()
}
}
// 等效的 try-finally 方式
fun readFileManual(path: String): String? {
val reader = java.io.File(path).bufferedReader()
return try {
reader.readText()
} catch (e: Exception) {
null
} finally {
reader.close()
}
}
4. 不要忽略异常
// 不推荐:空 catch 块
fun bad() {
try {
someRiskyOperation()
} catch (e: Exception) {
// 忽略异常 - 这会隐藏问题!
}
}
// 推荐:至少记录异常
fun good() {
try {
someRiskyOperation()
} catch (e: Exception) {
println("操作失败: ${e.message}")
// 或者重新抛出
// throw e
}
}
fun someRiskyOperation() {}
5. 异常用于异常情况
// 不推荐:用异常控制流程
fun findUser(id: Long): User {
val user = database.find(id)
if (user == null) {
throw UserNotFoundException(id) // 正常情况不应抛异常
}
return user
}
// 推荐:使用可空类型或 Result
fun findUserSafe(id: Long): User? {
return database.find(id)
}
fun findUserResult(id: Long): Result<User> {
val user = database.find(id)
return if (user != null) {
Result.Success(user)
} else {
Result.Failure(AppError.UserNotFoundError(id))
}
}
// 数据库模拟
object database {
fun find(id: Long): User? = if (id > 0) User(id) else null
}
data class User(val id: Long)
class UserNotFoundException(id: Long) : Exception("User $id not found")
6. 链式异常
fun main() {
fun loadData(): String {
try {
// 尝试读取文件
java.io.File("nonexistent.txt").readText()
} catch (e: java.io.IOException) {
// 包装异常,保留原始原因
throw DataLoadingException("加载数据失败", e)
}
return ""
}
try {
loadData()
} catch (e: DataLoadingException) {
println("错误: ${e.message}")
println("原因: ${e.cause?.message}")
}
}
class DataLoadingException(message: String, cause: Throwable?) :
Exception(message, cause)
常见异常类型
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
NullPointerException | 访问 null 引用 | str!!.length |
IllegalArgumentException | 非法参数 | require(x > 0) |
IllegalStateException | 非法状态 | check(isReady) |
IndexOutOfBoundsException | 索引越界 | list[100] |
NumberFormatException | 数字格式错误 | "abc".toInt() |
ArithmeticException | 算术错误 | 1 / 0 |
NoSuchElementException | 元素不存在 | emptyList.first() |
UnsupportedOperationException | 不支持的操作 | listOf(1).add(2) |
小结
本章我们学习了 Kotlin 异常处理的核心内容:
- 异常基础:层次结构、受检与非受检异常
- 抛出异常:
throw关键字、Nothing类型 - 捕获异常:
try-catch-finally、多 catch 块 - 预条件函数:
require、check、error - 自定义异常:继承
Exception、设计异常层次 - 最佳实践:特定异常、有意义信息、资源管理
Kotlin 的异常处理设计简洁实用,结合预条件函数可以编写出更安全、更易读的代码。
练习
- 实现一个银行账户类,包含存款、取款方法,使用自定义异常处理余额不足等情况
- 使用
require和check函数重构参数验证代码 - 创建一个异常层次结构,表示不同类型的网络错误
- 使用
Result类型替代异常来表示可能失败的操作 - 实现一个资源管理类,确保在异常情况下资源被正确释放