异常 异常有助于你的代码在发生运行时错误时仍能按预期运行,即使这些错误可能会中断程序的执行。 Kotlin 默认将所有异常视为 unchecked (未受检)异常。 未受检异常简化了异常处理过程:你可以捕获异常,但你不需要显式处理或 声明 它们。
处理异常主要包括两个步骤:
异常由 Exception
类的子类表示, Exception
类是 Throwable
类的子类。 关于层次结构的更多信息,请参阅 异常层次结构 章节。 由于 Exception
是一个 open class
,你可以创建 自定义异常 以满足你应用程序的特定需求。
抛出异常 使用 throw
关键字可以手动抛出异常。 抛出异常表示代码中发生了意外的运行时错误。 异常是 对象 ,抛出异常将创建异常类的一个实例。
你可以不带任何参数地抛出异常:
throw IllegalArgumentException()
为了更好地理解问题的来源,可以包含额外的信息,如自定义消息和原始原因:
val cause = IllegalStateException("原始原因:非法状态")
// 如果 userInput 是负数,则抛出 IllegalArgumentException
// 同时显示原始原因,由 cause IllegalStateException 表示
if (userInput < 0) {
throw IllegalArgumentException("输入必须是非负数", cause)
}
在这个例子中,当用户输入负数时抛出 IllegalArgumentException
。 你可以创建自定义错误消息,并保留异常的原始原因 (cause
),它将包含在 堆栈跟踪 中。
使用前置条件函数抛出异常 Kotlin 提供了额外的方式来自动抛出异常,使用前置条件函数。 前置条件函数包括:
这些函数适用于当特定条件不满足时程序流程无法继续的情况。这简化了代码并使这些检查的处理更加高效。
require()
函数使用 require()
函数在函数的操作中验证输入参数的重要性,如果这些参数无效则函数无法继续执行。
如果 require()
中的条件未满足,它会抛出 IllegalArgumentException
:
fun getIndices(count: Int): List<Int> {
require(count >= 0) { "Count must be non-negative. You set count to $count." }
return List(count) { it + 1 }
}
fun main() {
// 这将抛出 IllegalArgumentException
println(getIndices(-1))
// 取消下面一行的注释以查看一个有效的例子
// println(getIndices(3))
// [1, 2, 3]
}
require()
函数允许编译器执行 智能类型转换 。 在成功检查之后,变量会自动转换为非空类型。 这些函数常用于空值性检查,以确保变量在继续执行之前不为 null。例如:
fun printNonNullString(str: String?) {
// 空值性检查
require(str != null)
// 在成功检查后,'str' 保证不为 null,并自动智能转换为非空的 String
println(str.length)
}
check()
函数使用 check()
函数来验证对象或变量的状态。 如果检查失败,表示存在需要解决的逻辑错误。
如果 check()
函数中指定的条件为 false
,它会抛出 IllegalStateException
:
fun main() {
var someState: String? = null
fun getStateValue(): String {
val state = checkNotNull(someState) { "State must be set beforehand!" }
check(state.isNotEmpty()) { "State must be non-empty!" }
return state
}
// 如果取消下面一行的注释,程序将抛出 IllegalStateException
// getStateValue()
someState = ""
// 如果取消下面一行的注释,程序将抛出 IllegalStateException
// getStateValue()
someState = "non-empty-state"
// 这将打印 "non-empty-state"
println(getStateValue())
}
check()
函数允许编译器执行 智能类型转换 。 在成功检查之后,变量会自动转换为非空类型。 这些函数常用于空值性检查,以确保变量在继续执行之前不为 null。例如:
fun printNonNullString(str: String?) {
// 空值性检查
check(str != null)
// 在成功检查后,'str' 保证不为 null,并自动智能转换为非空的 String
println(str.length)
}
error()
函数error()
函数用于信号非法状态或代码中逻辑上不应发生的条件。 适用于在代码中有意抛出异常的场景,例如当代码遇到意外状态时。 这个函数在 when
表达式中特别有用,提供了一种清晰的方式来处理逻辑上不应发生的情况。
在以下示例中, error()
函数用于处理未定义的用户角色。 如果角色不是预定义的角色之一,将抛出 IllegalStateException
:
class User(val name: String, val role: String)
fun processUserRole(user: User) {
when (user.role) {
"admin" -> println("${user.name} 是管理员。")
"editor" -> println("${user.name} 是编辑。")
"viewer" -> println("${user.name} 是查看者。")
else -> error("未定义的角色: ${user.role}")
}
}
fun main() {
// 这按预期工作
val user1 = User("Alice", "admin")
processUserRole(user1)
// Alice 是管理员。
// 这将抛出 IllegalStateException
val user2 = User("Bob", "guest")
processUserRole(user2)
}
使用 try-catch 块处理异常 当抛出异常时,它会中断程序的正常执行。 你可以使用 try
和 catch
关键字优雅地处理异常,以保持程序的稳定。 try
块包含可能抛出异常的代码,而 catch
块捕获并处理异常(如果发生的话)。 异常会被第一个匹配其特定类型或异常的 超类 的 catch
块捕获。
下面是如何将 try
和 catch
关键字一起使用的示例:
try {
// 可能抛出异常的代码
} catch (e: SomeException) {
// 处理异常的代码
}
常见的方法是将 try-catch
作为表达式使用,这样它可以从 try
块或 catch
块中返回一个值:
fun main() {
val num: Int = try {
// 如果 count() 成功完成,其返回值将赋值给 num
count()
} catch (e: ArithmeticException) {
// 如果 count() 抛出异常,catch 块返回 -1,
// 并将其赋值给 num
-1
}
println("结果: $num")
}
// 模拟一个可能抛出 ArithmeticException 的函数
fun count(): Int {
// 修改此值以返回不同的 num 值
val a = 0
return 10 / a
}
你可以为同一个 try
块使用多个 catch
处理器。 可以添加尽可能多的 catch
块来处理不同的异常。 当有多个 catch
块时,重要的是按从最具体到最一般的异常顺序排列它们,遵循从上到下的顺序。 这种排序与程序的执行流程一致。
考虑以下示例中的 自定义异常 :
open class WithdrawalException(message: String) : Exception(message)
class InsufficientFundsException(message: String) : WithdrawalException(message)
fun processWithdrawal(amount: Double, availableFunds: Double) {
if (amount > availableFunds) {
throw InsufficientFundsException("余额不足,无法进行取款。")
}
if (amount < 1 || amount % 1 != 0.0) {
throw WithdrawalException("无效的取款金额。")
}
println("取款处理完成")
}
fun main() {
val availableFunds = 500.0
// 修改此值以测试不同的场景
val withdrawalAmount = 500.5
try {
processWithdrawal(withdrawalAmount.toDouble(), availableFunds)
// catch 块的顺序很重要!
} catch (e: InsufficientFundsException) {
println("捕获到 InsufficientFundsException: ${e.message}")
} catch (e: WithdrawalException) {
println("捕获到 WithdrawalException: ${e.message}")
}
}
处理 WithdrawalException
的通用 catch
块会捕获所有该类型的异常,包括像 InsufficientFundsException
这样的特定异常,除非它们已经被更具体的 catch
块捕获。
finally
块finally
块包含的代码会始终执行,无论 try
块是否成功完成或抛出异常。 使用 finally
块可以在执行完 try
和 catch
块后进行清理。 当处理文件或网络连接等资源时,这尤其重要,因为 finally
确保这些资源被正确关闭或释放。
以下是如何通常将 try-catch-finally
块一起使用的示例:
try {
// 可能抛出异常的代码
}
catch (e: YourException) {
// 异常处理器
}
finally {
// 始终执行的代码
}
try
表达式的返回值由 try
或 catch
块中最后执行的表达式决定。 如果没有发生异常,结果来自 try
块;如果处理了异常,则结果来自 catch
块。 finally
块始终执行,但不会改变 try-catch
块的结果。
让我们看一个示例来演示:
fun divideOrNull(a: Int): Int {
// try 块始终会被执行
// 如果发生异常(如除零错误),会立即跳转到 catch 块
try {
val b = 44 / a
println("try 块: 执行除法: $b")
return b
}
// catch 块由于 ArithmeticException(如果 a == 0 则为除零错误)被执行
catch (e: ArithmeticException) {
println("catch 块: 遇到 ArithmeticException $e")
return -1
}
finally {
println("finally 块: finally 块始终会被执行")
}
}
fun main() {
// 修改此值以获取不同的结果。ArithmeticException 将返回: -1
divideOrNull(0)
}
在 Kotlin 中,处理实现 AutoClosable
接口的资源 (如 FileInputStream
或 FileOutputStream
)的惯用方法是使用 .use()
函数。 该函数在代码块完成后自动关闭资源,无论是否抛出异常,从而消除了对 finally
块的需求。 因此,Kotlin 不需要像 Java 的 try-with-resources 那样的特殊语法来进行资源管理。
FileWriter("test.txt").use { writer ->
writer.write("some text")
// 在此代码块之后,.use 函数会自动调用 writer.close(),类似于 finally 块
}
如果你的代码需要在不处理异常的情况下进行资源清理,你也可以使用带有 finally
块的 try
,而不使用 catch
块:
class MockResource {
fun use() {
println("资源正在使用中")
// 模拟资源的使用
// 如果发生除零错误则会抛出 ArithmeticException
val result = 100 / 0
// 如果抛出异常,则此行不会被执行
println("结果: $result")
}
fun close() {
println("资源已关闭")
}
}
fun main() {
val resource = MockResource()
//sampleStart
try {
// 尝试使用资源
resource.use()
} finally {
// 确保资源始终被关闭,即使发生异常
resource.close()
}
// 如果抛出异常,则此行不会被打印
println("程序结束")
//sampleEnd
}
如你所见, finally
块保证了资源被关闭,无论是否发生异常。
在 Kotlin 中,你可以根据具体需要选择只使用 catch
块、只使用 finally
块或同时使用两者,但 try
块必须至少伴随一个 catch
块或一个 finally
块。
创建自定义异常 在 Kotlin 中,你可以通过创建继承自内置 Exception
类的类来定义自定义异常。 这允许你创建更具体的错误类型,以满足应用程序的需求。
要创建自定义异常,你可以定义一个继承自 Exception
的类:
class MyException: Exception("我的消息")
在这个示例中,有一个默认的错误消息 "我的消息",但如果需要,你也可以留空。
在 Kotlin 中,异常是有状态的对象,携带特定于其创建上下文的信息,这称为 堆栈跟踪 。 避免使用 对象声明 来创建异常。 相反,每次需要异常时都创建一个新的实例。 这样,你可以确保异常的状态准确反映特定的上下文。
自定义异常也可以是任何已存在的异常子类的子类,例如 ArithmeticException
子类:
class NumberTooLargeException: ArithmeticException("我的消息")
如果你想创建自定义异常的子类,你必须将父类声明为 open
, 因为 类默认是 final 的 ,否则不能被子类化。
例如:
// 将自定义异常声明为 open 类,使其可以被子类化
open class MyCustomException(message: String): Exception(message)
// 创建自定义异常的子类
class SpecificCustomException: MyCustomException("特定错误消息")
自定义异常的行为与内置异常一样。你可以使用 throw
关键字抛出它们,并通过 try-catch-finally
块进行处理。 下面是一个示例:
class NegativeNumberException: Exception("参数小于零。")
class NonNegativeNumberException: Exception("参数为非负数。")
fun myFunction(number: Int) {
if (number < 0) throw NegativeNumberException()
else if (number >= 0) throw NonNegativeNumberException()
}
fun main() {
// 修改此函数中的值以获取不同的异常
myFunction(1)
}
在具有多种错误场景的应用程序中,创建异常层次结构可以使代码更清晰和更具针对性。 你可以通过使用 抽象类 或 密封类 作为通用异常功能的基类,并为详细的异常类型创建具体的子类来实现。 此外,具有可选参数的自定义异常提供了灵活性,允许用不同的消息进行初始化,从而实现更细粒度的错误处理。
让我们看一个示例,使用密封类 AccountException
作为异常层次结构的基类, APIKeyExpiredException
类作为子类,展示了如何使用可选参数来提供更详细的异常信息:
//sampleStart
// 创建一个密封类作为与账户相关的错误的异常层次结构的基类
sealed class AccountException(message: String, cause: Throwable? = null):
Exception(message, cause)
// 创建 AccountException 的子类
class InvalidAccountCredentialsException : AccountException("检测到无效的账户凭证")
// 创建 AccountException 的子类,允许添加自定义消息和原因
class APIKeyExpiredException(message: String = "API 密钥过期", cause: Throwable? = null) : AccountException(message, cause)
// 修改占位符函数的值以获得不同的结果
fun areCredentialsValid(): Boolean = true
fun isAPIKeyExpired(): Boolean = true
//sampleEnd
// 验证账户凭证和 API 密钥
fun validateAccount() {
if (!areCredentialsValid()) throw InvalidAccountCredentialsException()
if (isAPIKeyExpired()) {
// 示例:抛出带有特定原因的 APIKeyExpiredException
val cause = RuntimeException("由于网络错误,API 密钥验证失败")
throw APIKeyExpiredException(cause = cause)
}
}
fun main() {
try {
validateAccount()
println("操作成功:账户凭证和 API 密钥有效。")
} catch (e: AccountException) {
println("错误:${e.message}")
e.cause?.let { println("原因:${it.message}") }
}
}
Nothing 类型 在 Kotlin 中,每个表达式都有一个类型。 表达式 throw IllegalArgumentException()
的类型是 Nothing
, 这是一个内置类型,是所有其他类型的子类型,也被称为 底层类型 。 这意味着 Nothing
可以用作返回类型或泛型类型,适用于任何其他类型而不会引起类型错误。
Nothing
是 Kotlin 中的一个特殊类型,用于表示永远不会成功完成的函数或表达式, 无论是因为它们总是抛出异常还是进入无限执行路径,如无限循环。 你可以使用 Nothing
来标记尚未实现的函数或设计为总是抛出异常的函数, 清晰地向编译器和代码读者表明你的意图。 如果编译器在函数签名中推断出 Nothing
类型,它会发出警告。 显式地定义 Nothing
作为返回类型可以消除这种警告。
这段 Kotlin 代码展示了 Nothing
类型的使用,其中编译器标记了函数调用后的代码为不可达代码:
class Person(val name: String?)
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
// 这个函数永远不会成功返回。
// 它总是会抛出异常。
}
fun main() {
// 创建一个 Person 实例,其中 'name' 为 null
val person = Person(name = null)
val s: String = person.name ?: fail("Name required")
// 此时 's' 保证已被初始化
println(s)
}
Kotlin 的 TODO()
函数,也使用了 Nothing
类型,作为占位符,突出显示代码中需要将来实现的区域:
fun notImplementedFunction(): Int {
TODO("This function is not yet implemented")
}
fun main() {
val result = notImplementedFunction()
// 这会抛出 NotImplementedError
println(result)
}
如你所见, TODO()
函数总是会抛出一个 NotImplementedError
异常。
异常类 让我们来探索一些 Kotlin 中常见的异常类型,它们都是 RuntimeException
类的子类:
ArithmeticException
: 当无法执行某个算术操作时,比如除以零时,就会发生这个异常。
val example = 2 / 0 // 抛出 ArithmeticException
IndexOutOfBoundsException
: 这个异常在某种索引超出范围时抛出,比如数组或字符串的索引超出了有效范围。
val myList = mutableListOf(1, 2, 3)
myList.removeAt(3) // 抛出 IndexOutOfBoundsException
为了避免这个异常,可以使用更安全的替代方案,比如 getOrNull()
函数:
val myList = listOf(1, 2, 3)
// 返回 null,而不是 IndexOutOfBoundsException
val element = myList.getOrNull(3)
println("Index 3 处的元素: $element")
NoSuchElementException
: 当访问某个集合中不存在的元素时,就会抛出这个异常。 它发生在使用期望特定元素的方法时,例如 first()
、 last()
或 elementAt()
。
val emptyList = listOf<Int>()
val firstElement = emptyList.first() // 抛出 NoSuchElementException
避免此异常的方法是使用更安全的替代方案,例如 firstOrNull()
函数:
val emptyList = listOf<Int>()
// 返回 null,而不是 NoSuchElementException
val firstElement = emptyList.firstOrNull()
println("空列表中的第一个元素: $firstElement")
NumberFormatException
: 当尝试将字符串转换为数字类型,但字符串格式不正确时,就会发生这个异常。
val string = "This is not a number"
val number = string.toInt() // 抛出 NumberFormatException
为了避免这个异常,可以使用更安全的替代方案,例如 toIntOrNull()
函数:
val nonNumericString = "not a number"
// 返回 null,而不是 NumberFormatException
val number = nonNumericString.toIntOrNull()
println("转换后的数字: $number")
NullPointerException
: 当应用程序尝试使用值为 null
的对象引用时,就会抛出这个异常。 尽管 Kotlin 的空安全特性显著减少了 NullPointerException
的风险,但它们仍可能发生,例如通过故意使用 !!
操作符或与不具备 Kotlin 空安全特性的 Java 进行交互时。
val text: String? = null
println(text!!.length) // 抛出 NullPointerException
虽然 Kotlin 中所有异常都是未受检异常,你不必显式捕获它们,但你仍然可以根据需要捕获它们。
异常层次结构 Kotlin 异常层次结构的根是 Throwable
类。 它有两个直接子类, Error
和 Exception
:
RuntimeException
通常是由于程序代码中检查不充分造成的,可以通过编程手段来防止。 Kotlin 帮助防止常见的 RuntimeExceptions
,如 NullPointerException
,并提供编译时警告,以避免潜在的运行时错误,如除零错误。 下图展示了 RuntimeException
的子类型层次结构:
堆栈跟踪 堆栈跟踪 是由运行时环境生成的报告,用于调试。 它显示了导致程序中特定点的函数调用序列,尤其是发生错误或异常的地方。
让我们来看一个例子,在 JVM 环境中,由于异常,堆栈跟踪会自动打印出来:
fun main() {
//sampleStart
throw ArithmeticException("This is an arithmetic exception!")
//sampleEnd
}
在 JVM 环境中运行此代码会生成以下输出:
Exception in thread "main" java.lang.ArithmeticException: This is an arithmetic exception!
at MainKt.main(Main.kt:3)
at MainKt.main(Main.kt)
第一行是异常描述,包括:
在异常描述之后,以 at
开头的每一行都是堆栈跟踪。一行堆栈跟踪被称为 堆栈跟踪元素 或 堆栈帧 :
Kotlin 与 Java、Swift 和 Objective-C 的异常互操作性 由于 Kotlin 将所有异常视为未受检异常,这可能会导致从区分受检异常和未受检异常的语言中调用这些异常时出现复杂情况。 为了解决 Kotlin 与 Java、Swift 和 Objective-C 等语言之间的异常处理差异,你可以使用 @Throws
注解。 这个注解会提醒调用者可能会抛出异常。有关更多信息,请参见 从 Java 调用 Kotlin 和 与 Swift/Objective-C 的互操作性 。
Last modified: 26 十一月 2024