在现实生活中,当我们决定做某件事时,我们主要考虑的是成功的体验。例如,如果你开车去朋友家吃晚饭,你可能会在地图网站上查找路线,确保车里有足够的汽油,并在正确的时间出发,以便在晚餐热的时候到达。如果一切按计划进行,你就会准时到达。
然而,事情并不总是按计划进行。例如,如果在途中爆胎,你就必须换上备胎,去修理厂,并买一个新轮胎。等你做完所有这些事情,晚餐可能已经凉了,你甚至可能不得不取消计划。
与现实生活非常相似,当我们的 Kotlin 代码运行时,事情可能不会按计划进行。我们的函数通常代表我们的计划——我们告诉 Kotlin,”一般来说,按照这个计划执行。”如果出了什么问题,我们会做别的事情,作为该计划的异常。
因此,当代码中发生意外情况时,我们称之为异常。在本章中,我们将学习如何处理这些异常!
运行时问题
我们已经看到,错误可以在两个不同的时间出现——编译时或运行时。正如 Cecil 在上一章中发现的,编译时的错误非常有用,因为您可以在应用程序的用户遇到它之前修复它。
然而,并非所有问题都能在编译时检测到。例如,这里有一个函数可以将数字(如 3)转换为其序数(如 “third”)。
val ordinals = listOf("zeroth", "first", "second", "third", "fourth", "fifth")
fun ordinal(number: Int) = ordinals.get(number)看这个函数,很容易看出它只支持最多 5 的数字的序数。然而,Kotlin 编译器无法确定 number 参数是否会在这些范围内。我们可以很容易地用太高的数字调用这个函数。
fun main() {
val place = ordinal(9)
}运行这个 main() 函数会产生以下错误消息。
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of
bounds for length 6
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
at MainKt.ordinal(Main.kt:3)
at MainKt.main(Main.kt:7)
at MainKt.main(Main.kt)
计划是简单地返回数字的序数,但事情没有按计划进行。将 9 传递给这个函数导致了计划的异常。由于编译器无法阻止我们用超界的参数调用 ordinal(),我们不得不在运行时处理这个问题。
有许多类似的问题无法在编译时检测到。例如:
- 在编译时,Kotlin 无法知道映射是否包含特定的键。(请参阅清单 9.11)。
- 在编译时,Kotlin 无法知道当我们向数据库请求数据时会得到什么值。
- 在编译时,Kotlin 无法知道用户在提示时可能会在键盘上输入什么。我们可以要求用户输入邮政编码,但可能会得到电话号码。
正因如此,我们需要知道如何处理运行时可能出错的事情。要做到这一点,我们首先需要理解调用栈。
调用栈
当你的 Kotlin 程序运行时,一个地方的代码通常会调用另一个地方的代码,而另一个地方的代码又可能调用再一个地方的代码。为了演示这一点,让我们添加一个函数来宣布你计划做的任务的序数。我们不把这个函数命名为 announce(),而将其缩写为 annc(),这样在接下来的一些图表中会更合适。
val ordinals = listOf("zeroth", "first", "second", "third", "fourth", "fifth")
fun ordinal(number: Int) = ordinals.get(number)
fun annc(number: Int, task: String): String {
val ordinal = ordinal(number)
return "The $ordinal thing I will do is $task."
}
fun main() {
val first = annc(1, "clean my room")
// "The first thing I will do is clean my room."
}这段代码有一个简单的执行路径。从 main() 函数开始……
main()函数调用annc()函数。annc()函数然后调用ordinal()函数。ordinal()函数调用列表的get()函数。get()函数将其结果返回给ordinal()函数。ordinal()函数将其结果返回给annc()函数。annc()函数将其结果返回给main()函数。
当然,当 main() 函数结束时,程序成功完成运行。
这有点像我们的程序在爬山。它从 main() 函数内部的地面 level 开始,对于沿途的每个函数调用,它在山上爬得更高。最终,它进入 get() 函数,此时每个函数依次返回其值,程序沿着山往下走,回到 main() 函数中。
与 mountain 的图画不同,更简单的表示方式可能是一叠盒子。
上面的图表仍然大致是 mountain 的形状。每个小盒子代表一个函数。当程序的执行进行时(从左到右),每个函数要么调用另一个函数,要么返回一个值给调用它的函数。
当一个函数调用另一个函数时,下一步的盒子堆将在调用它的函数之上包括那个函数。
当一个函数返回时,下一步的盒子堆将从栈顶移除该函数。
请注意,这些是栈变化的唯一两种方式!盒子永远不能从除栈顶之外的任何地方添加或移除!在编程中,这个函数调用栈被称为调用栈,有时也简称为栈。栈中的每个盒子都是一个栈帧。
所以,这个图表显示了程序运行时每个步骤的调用栈是什么样子的。
我们从清单 17.3中的例子非常简单——我们只有几个函数,每个调用下一个,所以随着时间的推移,调用栈看起来像一座单独的山。然而,通常它看起来更像山脉。例如,让我们在 main() 函数中添加一个对 println() 的调用。
fun main() {
val first = annc(1, "clean my room")
println(first)
}The first thing I will do is clean my room.
通过这个小的改变,调用栈随时间的变化如下:
值得一提的是,从技术上讲,println() 函数本身会调用其他函数,所以如果我们完整地绘制所有这些调用,山脉会比上面的更加复杂!
在我们继续之前,最后要注意的是——在清单 17.3中,我们只有函数调用其他函数,但调用栈通常还包括其他东西。例如,当您调用类的构造函数时,该类的属性也会被初始化,当这种情况发生时,它也是调用栈的一部分。类似地,您可能从具有自定义 getter 函数的属性获取值。这些也将在调用栈上有自己的帧。
既然我们已经了解了调用栈,这与异常和错误消息有什么关系?
调用栈、异常和错误消息
让我们更新 main() 函数。不是传递数字 1,而是传递数字 9,这将超出 ordinal() 函数的范围。
fun main() {
val task = annc(9, "clean my room")
println(task)
}当运行此程序时,我们看到的不是”The ninth thing I will do is clean my room”,而是得到一个 ArrayIndexOutOfBoundsException,就像在清单 17.2中一样。这是错误消息的再次展示:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
Index 9 out of bounds for length 6
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
at MainKt.ordinal(Main.kt:2)
at MainKt.annc(Main.kt:5)
at MainKt.main(Main.kt:10)
at MainKt.main(Main.kt)
让我们检查当此代码运行时,调用栈如何随时间变化。
当事情崩溃时,调用栈上有四个帧。我们收到的错误消息包括异常发生时调用栈外观的快照。调用栈在特定时间点的快照被称为栈跟踪。
请注意,不计算栈跟踪的底部行,它显示的内容与上面的盒子堆相同。
对于栈跟踪中的每一帧,我们可以看到函数的名称、该函数所在文件的名称,以及调用下一个函数的代码的行号。
理解栈跟踪很容易,但让我们一次看一行。
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
这一行告诉我们,错误发生在名为 get() 的函数中,它位于名为 ArrayList 的类中。这个类属于 Java 的 API,被 Kotlin 的标准库使用,错误发生在第 4165 行。(是的,这有很多行!)
at MainKt.ordinal(Main.kt:2)
接下来,我们可以看到 get() 函数(从前一行)是从我们的 ordinal() 函数调用的,在我称为 Main.kt 的文件的第 2 行。
at MainKt.annc(Main.kt:5)
之后,ordinal()(从前一行)被 annc() 在 Main.kt 的第 5 行调用。
at MainKt.main(Main.kt:10)
而 annc() 被同一文件的第 10 行的 main() 函数调用。
at MainKt.main(Main.kt)
而这最后一行只是告诉我们一切都是从 Main.kt 文件开始的。
栈跟踪在您试图找出哪里出了问题时很有帮助,但它也向我们展示了可以在哪里添加代码来尝试阻止程序突然终止。
捕获异常
当异常出现时,这有点像一个孩子朝窗户扔棒球。如果没有人接住那个棒球,它就会撞到窗户上。
类似地,在我们的 Kotlin 代码中,当抛出异常时,如果没有人捕获异常,错误消息将被打印出来,程序将崩溃。
通常,我们不希望程序在遇到异常时崩溃。为了说明原因,让我们将单个任务替换为任务列表,并为每个任务打印一条公告。
fun main() {
val tasks = listOf(1 to "clean my room", 9 to "take out trash", 5 to "feed the dog")
tasks.forEach { (number, task) ->
println(annc(number, task))
}
}当运行此程序时,我们将看到以下输出:
The first thing I will do is clean my room.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of bounds for length 6
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
at MainKt.ordinal(Main.kt:2)
at MainKt.annc(Main.kt:5)
at MainKt.main(Main.kt:11)
at MainKt.main(Main.kt)
第一个任务被打印到屏幕上,但第二个任务导致异常,所以程序中止了。因为程序停止运行,所以它从未打印出喂狗的任务!如果程序即使在有异常的情况下也能继续运行,那就太好了。
为了让事情继续运行,我们需要捕获那个异常。换句话说,我们需要有人在棒球撞穿窗户之前接住它!
那么,我们如何捕获异常?嗯,我们可以在调用栈的任何帧放置一个棒球运动员。从抛出异常的帧开始,异常将一个接一个地经过每个栈帧,一直到程序开始的 main() 函数。在沿途的每一步,如果那个帧有一个棒球运动员,他或她可以捕获异常,防止窗户破裂。
例如,我们可以在 main() 函数中放置一个捕手,她会在异常撞击窗户之前将其捕获:
让我们添加一些代码来捕获异常!为此,我们将使用try 表达式,它看起来像这样:
这个表达式有几个关键部分:
- 它以
try关键字开头。 - 之后是
try块。这是我们放置可能抛出异常的代码的地方。 - 之后是
catch关键字。 - 括号内是异常参数,它用我们想要捕获的异常类型的名称和类型声明。我们稍后会详细了解这一点。
- 之后是
catch块。这是我们放置捕获异常时应运行的代码的地方。
首先,让我们把我们的捕手(即 try 表达式)放在 main() 函数中,她可以在异常打破窗户之前捕获它。当我们调用 annc() 时,最终可能会抛出异常。由于该调用可能抛出异常,我们将把它放在 try 块的内部。然后,在 catch 块内部,我们将简单地打印出出了问题。这就是我们最终得到的结果:
fun main() {
val tasks = listOf(1 to "clean my room", 9 to "take out trash", 3 to "feed the dog")
tasks.forEach { (number, task) ->
try {
println(annc(number, task))
} catch (exception: Exception) {
println("Something went wrong!")
}
}
}当我们再次运行它时,在处理”take out trash”时我们仍然会得到异常,但这次异常将被 catch 块处理。结果,我们看到:
The first thing I will do is clean my room. Something went wrong! The fifth thing I will do is feed the dog.
通过这个更改,我们防止了程序因错误消息而崩溃!这意味着我们仍然可以看到关于喂狗的任务!处理完所有三个任务后,程序成功结束。
打印出”Something went wrong!”总比什么都没有好,但当我们捕获异常时,还有更多我们可以做的事情。
异常是对象
异常是一个对象,我们可以像使用任何其他对象一样使用它的函数和属性。在上面的清单 17.7中,我们的 catch 块声明了一个名为 exception 的异常参数,该参数的类型是 Exception。
虽然这里使用了变量名 exception,但大多数开发人员更喜欢使用非常短的变量名来表示异常,例如 ex 或 just e。
try {
println(annc(number, task))
} catch (e: Exception) {
println("Something went wrong!")
}这个变量在 catch 块内可见,因此您可以根据需要与其交互。例如,每个 Exception 对象都有一个名为 message 的可空属性,因此让我们更新我们的 println() 以包含该消息。
try {
println(annc(number, task))
} catch (e: Exception) {
println("Something went wrong! ${e.message}")
}现在当运行此程序时,我们可以看到什么东西出错了,而不仅仅是看到某物出错了——在这种情况下,索引 9 超出了长度为 6 的数组的范围。
The first thing I will do is clean my room. Something went wrong! Index 9 out of bounds for length 6 The fifth thing I will do is feed the dog.
除了 message 属性外,Exception 对象还包括一个栈跟踪,您可以使用 printStackTrace() 函数打印出来。1
但是,我们并不局限于使用 Exception 类本身。我们可以扩展它,并包含我们想要的任何函数或属性!不过,在这之前,我们需要先学习如何抛出我们自己的异常。
抛出异常
除了捕获异常,我们还可以自己抛出它们。由于异常只是一个对象,我们可以像平常一样实例化它——通过调用其构造函数。我们不必向其构造函数传递任何参数,但给它一条描述问题的消息可能会有帮助。
val exception = Exception("No cleaning allowed on holidays!")一旦您有了一个异常实例,您可以使用 throw 关键字抛出该异常,所以我们可以这样做:
throw exception但是,异常的栈跟踪是在异常实例化的点确定的,而不是在抛出的点确定的。因此,我们通常不会像在清单 17.10中那样实例化并存储异常。相反,通常在抛出异常时实例化它,像这样。
throw Exception("No cleaning allowed on holidays!")让我们更新我们的 annc() 函数,这样如果 task 包含”clean”这个词,我们就会抛出这个异常。
fun annc(number: Int, task: String): String {
if ("clean" in task) throw Exception("No cleaning allowed on holidays!")
val ordinal = ordinal(number)
return "The $ordinal thing I will do is $task."
}使用来自清单 17.9的 try 表达式运行此程序会产生以下输出:
Something went wrong! No cleaning allowed on holidays! Something went wrong! Index 9 out of bounds for length 6 The fifth thing I will do is feed the dog.
我们的 try 表达式现在正在处理至少两种不同情况的异常:
- 当任务包含”clean”这个词时抛出的异常。
- 当索引超出范围时抛出的异常。
目前,我们以相同方式处理这两种异常,但我们可能希望不同地处理每种情况。为了区分这两种不同类型的异常,我们可以确保它们有不同的类型。
异常类型
虽然我们可以像在清单 17.13中那样直接实例化 Exception 类,但为我们抛出的每个独特异常类别扩展它通常是有帮助的。例如,我们可以创建一个专用于节假日的 Exception 子类,当我们不被允许清洁时。
class HolidayException(val task: String) : Exception("'$task' is not allowed on holidays")现在我们可以抛出 HolidayException 而不是一般的 Exception。
fun annc(number: Int, task: String): String {
if ("clean" in task) throw HolidayException(task)
// ...
}通过此更改,此程序现在处理两种异常:
ArrayIndexOutOfBoundsException,可能从列表的get()函数抛出。HolidayException,可能从我们的annc()函数抛出。
这些每一个都是 Exception 类的子类,尽管 ArrayIndexOutOfBoundsException 经过了一些中间类。下面是一个 UML 类图,显示这些类是如何关联的。
让我们再次看看我们的 try 表达式。
try {
println(annc(number, task))
} catch (e: Exception) {
println("Something went wrong! ${e.message}")
}异常参数的类型告诉 Kotlin 哪些种类的异常应该在这个 catch 块中处理。这就像告诉棒球运动员只接某种颜色的球,而忽略其他的。
在清单 17.16中,类型是 Exception,所以这个 catch 块将处理任何类型为 Exception 的异常,包括其任何子类型。因为 ArrayIndexOutOfBoundsException 和 HolidayException 都是 Exception 的子类型,所以这段代码将捕获两者这些异常。
但是,我们可以将此类型更改为更具体的内容。例如,让我们将其更改为 HolidayException。
try {
println(annc(number, task))
} catch (e: HolidayException) {
println("Something went wrong! ${e.message}")
}进行此更改后,我们可以运行代码,并在输出中看到 try 表达式捕获并处理了 HolidayException,但它没有捕获第二个任务引起的 ArrayIndexOutOfBoundsException。
所以,异常参数的类型决定哪些异常被捕获。请记住,catch 块将处理指定的类型,包括其任何子类型。
任何 catch 块没有捕获的异常类型将简单地继续到下一个栈帧,在那里另一个 try 表达式可能有机会捕获并处理它。与往常一样,如果没有人捕获异常,它就会撞穿窗户。
不同地处理多种异常类型
有时您可能希望在单个 try 表达式中处理多种类型的异常,但对每个异常进行不同的操作。例如,不是捕获 HolidayException 而忽略 ArrayIndexOutOfBoundsException,您可能希望 main() 函数同时处理两者,但在每种情况下打印不同的内容。
为此,我们可以根据需要简单地附加额外的 catch 块,像这样。
try {
println(annc(number, task))
} catch (e: HolidayException) {
println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
println("I can't count that high!")
}It's a holiday! I'm not going to clean my room today! I can't count that high! The fifth thing I will do is feed the dog.
请注意,当有多个 catch 块时,第一个具有匹配异常类型的块获胜——任何后续块都将被忽略,即使其异常参数的类型匹配。正因如此,考虑 catch 块的顺序是个好主意。例如,我们可以在最顶部添加一个 Exception 的 catch 块:
try {
println(annc(number, task))
} catch (e: Exception) {
println("I wasn't expecting this!")
} catch (e: HolidayException) {
println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
println("I can't count that high!")
}但是,如果我们这样做,后面两个 catch 块将是无意义的。因为 HolidayException 和 ArrayIndexOutOfBoundsException 是 Exception 的子类,它们将始终只匹配第一个块!
I wasn't expecting this! I wasn't expecting this! The fifth thing I will do is feed the dog.
所以,相反,如果一个 try 表达式要同时包含异常类和其一个或多个子类的情况,请将子类放在其相应超类之上,像这样:
try {
println(annc(number, task))
} catch (e: HolidayException) {
println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
println("I can't count that high!")
} catch (e: Exception) {
println("I wasn't expecting this!")
}通过此更改,HolidayException 将在第一个 catch 块中处理,ArrayIndexOutOfBoundsException 将在第二个 catch 块中处理,而任何其他类型的异常将在第三个 catch 块中处理。
求值 Try 表达式
在整个章节中,我们将 try-catch 称为表达式。您可能还记得,语句和表达式之间的区别在于表达式可以被求值——换句话说,它可以归约为一个值。由于 try 表达式确实是表达式,我们可以将它们赋值给变量、传递给函数,或从函数返回。
例如,目前,我们在 main() 函数中的 try 表达式在每种情况下都调用 println()。
try {
println(annc(number, task))
} catch (e: HolidayException) {
println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
println("I can't count that high!")
}不是在各处调用 println(),我们可以简单地让这个 try 表达式求值为一个 String。为此,我们只需移除每个 println(),并将结果赋值给一个变量。然后我们可以打印该变量。
val words: String = try {
annc(number, task)
} catch (e: HolidayException) {
"It's a holiday! I'm not going to ${e.task} today!"
} catch (e: ArrayIndexOutOfBoundsException) {
"I can't count that high!"
}
println(words)这正如您所期望的那样工作:
- 如果没有异常,
annc()的结果将被赋值给words变量。 - 如果有
HolidayException,则该块中的字符串将被赋值给words。(”今天是假日!……”) - 如果有
ArrayIndexOutOfBoundsException,则”我数不到那么高!”将被赋值给words。 - 如果有任何其他异常,它将不会在这里被捕获。
Try-Catch-Finally
从前,有一个友好的邻居叫 Tara。她为拥有一片郁郁葱葱、绿色、修整整齐的草坪而自豪。每周两次,她会打开室外水龙头开始浇灌草坪。有一天,打开水龙头后,她注意到洒水器没有按应有的方式旋转。
意识到洒水器坏了,她走进屋子,从网上商店订了一个新的。然后,她继续处理那天需要做的其他事情。不幸的是,到了一天结束时,她的草坪被水淹了,因为她从未关掉水龙头!
就像 Tara 一样,有时您需要确保完成一项任务,即使出了问题!以下代码模拟了 Tara 的经历:
try {
faucet.turnOn()
watch(sprinkler) // SprinklerBrokenException is thrown here
faucet.turnOff() // This never runs!
} catch (e: SprinklerBrokenException) {
store.orderNewSprinkler()
}这段代码看起来合理,但有一个问题——如果 watch() 函数抛出 SprinklerBrokenException,则 faucet.turnOff() 永远不会运行!
为了帮助处理这种情况,您可以在 try 表达式的末尾包含一个名为 finally 的块。finally 块将在表达式的其余部分被处理后运行,无论是否抛出异常。它是这样的:
try {
faucet.turnOn()
watch(sprinkler) // SprinklerBrokenException is thrown here
} catch (e: SprinklerBrokenException) {
store.orderNewSprinkler()
} finally {
faucet.turnOff() // This will run, even when the sprinkler breaks!
}通过此代码:
- 如果没有抛出异常,则
faucet.turnOff()将在watch(sprinkler)完成后运行。 - 如果抛出
SprinklerBrokenException,则faucet.turnOff()将在store.orderNewSprinkler()完成后运行。 - 如果抛出任何其他类型的异常,则
faucet.turnOff()将在异常向栈的起点传播之前运行。
finally 块通常对于需要关闭的资源最有帮助。例如,如果您的程序要读取计算机上的文件,您会希望确保在处理完毕后将其关闭,即使在处理过程中抛出异常。
请注意,即使没有任何 catch 块,您也可以将 finally 与 try 结合使用!例如,您可能希望异常由更接近栈开头的另一个 try 表达式处理,但仍然需要确保关闭水龙头。
try {
faucet.turnOn()
watch(sprinkler)
} finally {
faucet.turnOff()
}至少,try 表达式必须有一个 catch 块或一个 finally 块。
Try 表达式有点冗长。它们往往在屏幕上占用大量空间,并导致难以遵循的执行路径,因为并不总是能明显地看出异常将在哪里被处理。Kotlin 包含了另一种异常处理方法,这也值得考虑。
异常处理的函数式方法
Kotlin 的标准库包含一个名为 runCatching() 的函数。在内部,它使用 try 表达式,但通过使用这个函数,我们通常可以避免在代码中手动编写 try 表达式。使用这个函数很容易。首先,只需用可能抛出异常的代码调用它,并将其结果赋值给一个变量,像这样:
fun main() {
val tasks = listOf(1 to "clean my room", 9 to "take out trash", 5 to "feed the dog")
tasks.forEach { (number, task) ->
val result = runCatching { annc(number, task) }
}
}runCatching()将返回一个Result对象,其中包含两者之一——要么是成功的结果,要么是一个异常。在上述代码中,这意味着……
- 如果
annc()成功完成,则它将包含annc()返回的结果。 - 如果
annc()抛出异常,则它将包含该异常。
Result 对象上有几个函数,可用于处理结果或异常。处理它的一个简单方法是使用其 getOrDefault() 函数。例如,以下代码的作用与清单 17.8相同:
val result = runCatching { annc(number, task) }
val text = result.getOrDefault("Something went wrong!")
println(text)在这段代码中,如果 annc() 成功完成,则其结果将被赋值给 text 变量。否则字符串”Something went wrong!”将被赋值给它。这在很多情况下都能正常工作,但如果我们需要的默认值是基于异常的属性——例如,如果我们想包含异常的 message——那么我们需要别的东西。
对于这种情况,我们可以使用 getOrElse()。这个函数接受一个 lambda,您可以在其中操作异常对象。2 例如,下面的代码与清单 17.9的作用相同。
val result = runCatching { annc(number, task) }
val text = result.getOrElse { "Something went wrong! ${it.message}" }
println(text)Result 类包含许多其他函数,可用于转换其值或异常。这些函数可以链接在一起,就像集合操作链一样。
runCatching() 和 Result 可以使某些代码比等效的 try 表达式更容易阅读。但是,如果我们自己编写 try 表达式,我们会获得更多控制——我们可以捕获特定类型的异常,并在需要时添加 finally 块。
此外,来自函数式编程背景的开发人员通常需要更高级的功能,例如仅捕获某些类型异常的能力或累积多个异常的能力。如果这是您,您可能需要查看 Kotlin 的 Arrow 函数式编程库。
总结
您在本章中出色地完成了关于异常的学习,包括:
- 为什么我们会在运行时遇到问题。
- 什么是调用栈,以及如何阅读栈跟踪。
- 如何捕获异常,以及一旦获得异常后如何处理它。
- 如何抛出我们自己的异常。
- 如何捕获不同类型的异常。
- try-catch 是一个表达式,因此可以赋值给变量。
- 如何使用 finally 块来确保某些事情发生,无论成功或失败。
- 如何使用 runCatching() 来进行更函数式的异常处理方法。
在下一章中,我们将最终仔细研究泛型!到时候见!