本文收錄于 kotlin入門潛修專題系列,歡迎學習交流。
創作不易,如有轉載,還請備注。
java中異常
很多語言都有異常機制,異常能夠改變正常的程序執行流程,主要用于終止一些非法的邏輯流程。這些流程如果我們不及時終止,則可能會引起后續的一系列錯誤甚至程序崩潰。
我們都知道java中有兩類異常,一類是受檢異常,另一類是運行時異常。受檢異常是指,在編譯期間就必須要進行顯示處理的異常,否則就會編譯不通過;而運行時異常則在運行期間發生的異常,如果不處理會直接導致程序崩潰。來看個java異常的例子:
public class JavaMain {
//main方法
public static void main(String[] args) {
testException();
}
//受檢異常,jdk在代碼聲明的時候,就已經顯示拋出的異常
//這里異常是FileNotFoundException,該方法的處理方式是繼續向上拋出
//這里我們故意寫了一個不存在的文件路徑:a/b/filename
//在代碼執行的時候就會拋出FileNotFoundException異常
private static void writeFile() throws FileNotFoundException {
OutputStream os = new FileOutputStream("a/b/filename");
}
//受檢異常,同wiretFIle方法,只不過這里的處理方式是自己處理(try-catch)
private static void writeFile2() {
try {
OutputStream os = new FileOutputStream("a/b/filename");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//運行時異常,編譯期將不會強制對該類型異常進行處理
//而是在運行的時候,如果非法則拋出對應異常(這里是ArithmeticException),
//當然,在這里,我們也可以在編寫代碼的時候
//就是用try-catch塊將其包括,這樣可以避免程序崩潰
private static void divisionByZero() {
int i = 10 / 0;
}
//異常測試方法
private static void testException() {
try {
writeFile();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
writeFile2();
divisionByZero();
}
}
上面代碼中,注釋已經寫的比較清晰,這不再闡述代碼的意義。
關于受檢異常還需要表明一點,受檢異常可以有兩種處理方式:一種是自己不處理,繼續像上層拋出異常(見writeFile方法),如果上層處理不了,則還要繼續向上拋出,直到能處理為止(或許會到程序的執行入口main方法),也就是說,這種方法終歸要在一個地方進行異常處理;另一種則是自己處理掉異常,即使用try-catch包裹可能出現異常的代碼塊,見(writeFile2方法),這種方式則不必再繼續向上拋出。具體采用哪種方法,需要根據實際場景進行選擇。
kotlin中的異常
在上一小節,我們簡單演示了java中的異常概念,因為我們的主題是kotlin,所以不再展開java異常,本節開始對kotlin中的異常進行闡述。
kotlin中的所有異常都繼承自一個類:Throwable,先來看看其定義:
public open class Throwable(open val message: String?, open val cause: Throwable?) {
constructor(message: String?) : this(message, null)
constructor(cause: Throwable?) : this(cause?.toString(), cause)
constructor() : this(null, null)
}
從Throwable的構造方法可以看出,kotlin中的異??梢越邮苊枋鲂畔?,同時也可以接收另一個異常入參,用于表示該異常的引起原因。
kotlin中的異常同樣有try-catch-finally流程處理機制,try即表示嘗試執行其代碼塊;catch則用于捕獲try塊中的代碼可能出現的異常;而finally代碼塊則表示其中的代碼,無論如何都會被執行到的,我們一般會在finally代碼塊中做些資源釋放等操作。
來看個kotlin中異常的例子:
fun main(args: Array<String>) {
test()
}
//測試方法,顯示拋出一個異常
fun test() {
throw Exception("test exception...")
}
上面代碼在執行的時候,就會拋出描述信息為test exception...的異常。由于我們沒有對該異常做任何處理,所以該異常會直接導致程序crash。如果我們避免程序crash,則需要利用try-catch-finally機制進行異常捕獲。
這里先說下try-catch-finally三者之間的關系。首先,要處理異常必須要使用try來包裹代碼塊;其次,try后面必須跟著catch或者finally中的至少一個,當然也可以同時存在。來看個例子:
fun main(args: Array<String>) {
//使用了try-catch進行包裹
try {
test()
} catch (e: Exception) {//catch塊可以捕獲try塊中的異常
println(e.message)//打印異常信息
}
}
//拋出異常的test方法
fun test() {
throw Exception("test exception...")
}
我們同樣可以在try塊后只跟finally塊,但是,finally塊的意義更多的是用于做些資源回收之類的操作,并不能阻止程序異常退出,如下所示:
fun main(args: Array<String>) {
try {
test()
} finally {
println("in finally...")
}
}
fun test() {
throw Exception("test exception...")
}
上面代碼會首先打印"in finally..."字符串,然后就會異常退出。所以說finally是無法阻止程序的異常退出的。
當然,我們可以同時使用try-catch-finally,這樣我們既能處理異常,保證程序不崩潰,又可以在發生異常時及時回收資源。
經典的異常處理流程問題
在本小節,來看一個經典的異常處理流程問題,先上代碼,如下所示:
//測試方法
fun test(): Int {
var i = 1
try {
i = 2
println("in try block: i = 2")
return i
} catch (e: Exception) {
i = 3
} finally {
i = 4
println("in finally block: i = 4")
}
return i
}
//main方法
fun main(args: Array<String>) {
println(test())
}
猜測下上面代碼會打印什么?不賣關子,上面代碼打印如下:
in try block: i = 2
in finally block: i = 4
2
由此可知,方法的調用首先執行了try塊中的語句,最后執行了finally代碼塊中的語句,并且確實改變了i的值。然而,當方法返回的時候,我們發現i的值卻是try代碼塊中的值,這有點不符合打印邏輯(畢竟我們在finally塊中改變了i值!),為什么?
先不解釋為什么,再來看個例子:
//測試方法
fun test2(): Int {
var i = 1
try {
i = 2
println("in try block: before i = i / 0")
i = i / 0
println("in try block: after i = i / 0")
return i
} catch (e: Exception) {
i = 3
println("in catch block: i = 2")
return i
} finally {
i = 4
println("in finally block: i = 4")
}
}
//main方法
fun main(args: Array<String>) {
println(test2())
}
上面代碼執行過后,打印結果如下所示:
in try block: before i = i / 0
in catch block: i = 2
in finally block: i = 4
3
上面這個例子,我們在try代碼塊以及catch代碼塊中都指定了return語句,顯然,try代碼塊中的return語句會被異常中斷,之后進入到catch塊處理。最后,我們同樣在finally塊中改變了要返回的i的值,然而,我們發現return的i值卻依然是catch塊中的i值,而不是finally塊修改后的i值,這是為什么?
先不著急解釋上面兩個例子return值為什么沒有被改變,這里先結合上面兩個例子的輸出,對異常的執行流程做出以下總結:
- 異常確實能夠改變程序的正常執行流程,在發生異常的地方,其后邊的語句都會被終止執行,轉而執行catch(如果有的話)或者finally(如果有的話)中的代碼塊。
- 對于沒有返回值的方法(即沒有return語句),無論是try代碼塊、catch代碼塊還是finally代碼塊中執行的代碼都會生效,而且以最后一個代碼塊執行的結果為準。
- 對于有返回值的方法,代碼的執行結果,會以第一個有效的return語句返回值為準。這句話意思是說,如果try語句正常執行,則以其內的return返回值為準;如果try再執行return之前拋出了異常,則以catch塊中的返回值為準,否則以finally塊的返回值為準。
總結也總結完了,那么上述流程的背后原理是什么?想要了解其背后的原理,可以結合字節碼來看一下。
照例,先貼出我們要分析的源代碼:
fun test2(): Int {
var i = 1
try {
i = 2
i = i / 0
println("in try block: after i = " + i)
return i
} catch (e: Exception) {
i = 3
println("in catch block: i = " + i)
return i
} finally {
i = 4
println("in finally block: i = " + i)
}
}
然后,貼出上述代碼對應的字節碼,字節碼比較長,只需要關注以下幾個節點(參見字節碼中的注釋)即可把整個流程串起來,如下所示:
public final static test2()I
TRYCATCHBLOCK L0 L1 L2 java/lang/Exception
TRYCATCHBLOCK L0 L1 L3 null
TRYCATCHBLOCK L2 L4 L3 null
TRYCATCHBLOCK L3 L5 L3 null
L6
LINENUMBER 9 L6
ICONST_1
ISTORE 0//!!!環節1,存儲常量1到局部變量表索引0處,即i=1
L7
LINENUMBER 10 L7
L0
NOP
L8
LINENUMBER 11 L8
ICONST_2
ISTORE 0//!!!環節2,存儲常量2到局部變量表索引0處,即i=2
L9
LINENUMBER 12 L9
ILOAD 0
ICONST_0//常量0
IDIV//!!!環節3,執行除以0的計算,即 i = i / 0,這里顯然會拋出運行是異常,意味著下面這條字節碼根本不會被執行
ISTORE 0 //!!!環節4,這個實際不會被執行!存儲計算結果到局部變量表0索引處
L10
LINENUMBER 13 L10
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "in try block: after i = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1//!!!環節5,存儲字符串到局部變量表索引為1的位置,字符串對應于"in try block: after i = " + i
L11
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 1//!!!環節6,將存儲的字符串加載到操作數棧頂,打印
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V//執行打印
L12
L13
LINENUMBER 14 L13
ILOAD 0//!!!環節6,將局部變量表索引為0的值加載到操作數棧,即加載的是i的值
ISTORE 1//環節7,存儲到局部變量表索引為1的位置
L1
LINENUMBER 20 L1
ICONST_4//!!!環節8,神奇!!,這個實際上對應的是finally塊中的i=4
ISTORE 0//將4存儲到局部變量表為0的位置
L14
LINENUMBER 21 L14
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "in finally block: i = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 0!!!環節9,加載環節7中存儲的4,用于字符串拼接,即對應于 println("in finally block: i = " + i)語句中的i值的獲取
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 2//!!!環節9,將字符串值存儲到局部變量表2索引處
L15
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 2//這個就是環節9中的值
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
L16
L17
ILOAD 1//!!!環節10,注意,這里加載的是局部變量索引1處的值,最近的一次存儲是在環節7處,顯然和finally代碼塊中的代碼沒有關系了
//因為finally中的代碼實際上將i的值存在了局部變量表的索引0處
IRETURN//!!!環節11,返回i的值,實際上對應于try塊。
L2
LINENUMBER 15 L2
ASTORE 1
L18
LINENUMBER 16 L18
ICONST_3
ISTORE 0!!!環節12,這個實際上對應的是catch塊中的代碼,即 i = 3
L19
LINENUMBER 17 L19//下面的打印語句同try中的一致,不再展開
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "in catch block: i = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 2
L20
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 2
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V//catch中的打印語句結束
L21
L22
LINENUMBER 18 L22
ILOAD 0//!!!環節13,這個加載的是環節12中保存的i值,即3
ISTORE 2//!!!環節14,將該值存到了局部變量表中索引2的位置
L4
LINENUMBER 20 L4//下面實際上對應的又是finally代碼塊中的位置!!!,和try之后的基本一樣,不再展開
ICONST_4
ISTORE 0
L23
LINENUMBER 21 L23
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "in finally block: i = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 3
L24
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 3
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
L25
L26
ILOAD 2//!!!環節15,將環節14中存儲的i值,加載到操作數棧
IRETURN//!!!環節16,返回該值
L27
LINENUMBER 22 L27
L3
ASTORE 1
L5
LINENUMBER 20 L5//下面實際上又是finally塊對應的代碼!!,不再展開。
ICONST_4
ISTORE 0
L28
LINENUMBER 21 L28
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "in finally block: i = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 2
L29
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 2
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
L30
L31
ALOAD 1//!!!環節17,這個局部變量表1中的值,實際上是環節7存儲的i值,在異常發生的時候,會先存儲該值。
ATHROW//!!!環節18,這個是拋出異常指令,如果try中發生異常,則會執行該指令
字節碼中的注釋已經很詳細了,如果仔細看應該能夠理解,不理解的話也沒有關系,這里給出一個宏觀的總結:
- 如果有finally代碼塊,則finally代碼塊會被多次編譯,其插入位置分別位于try塊之后、catch塊之后,同時其自身所處的代碼位置也會被再次編譯一次。也就是說,最多可被編譯3次。這也是為什么finally一定會被執行的原因。
- 含有return語句的try-catch-finally代碼,其執行流程實際上確實和代碼的書寫順序保持一致(這里的一致是指try-catch-finally的執行順序),但是如果每個塊中有返回值,則以第一個有效的return值為準(字節碼注釋已經詳細進行了說明)。這是為什么?
要解答這個問題,則需要結合上面的第一點,即如果有finally代碼塊,則無論是try塊還是catch塊,后面都會被插入finally代碼,進而會按照try-finally或者catch-finally的代碼塊順序來執行,只不過try-catch-finally中的局部變量值,在局部變量表中的存儲位置不同,而編譯器在執行返回的時候,實際上執行的是,第一個有效return語句對應的局部變量表的值。這里之所以加上“有效”兩個字,主要是考慮到了多種場景。比如,如果try中有return語句,在執行該語句之前沒有拋出任何異常,則一定會執行其return語句;但是如果再執行該語句之前拋出了異常,則會進到catch塊,不再執行其后的return語句;如果catch塊中有return語句,則會執行其return語句;最后才會執行方法體最后的return語句,這就是整個異常執行的流程。
實際上,java中的try-catch-finally執行機制也是如此。
kotlin異常機制的注意點
本小節闡述幾個kotlin中異常需要注意的幾個點。
無受檢異常。
kotlin異常機制同java最明顯的不同就是不再有受檢異常。kotlin給出了很多不提供受檢異常的理由,這里不再闡述。從個人觀點來看,受檢異常最大的壞處就是在每次寫代碼的時候都要顯示進行異常處理,無論實際上有沒有可能發生該異常,所以這就意味著會做很多無用功,而且代碼看著及其不簡潔(自行腦補下try-catch-finally...,如果還不夠,再腦補下嵌套的try-catch...)。
異常表達式
在kotlin中,try是一個表達式,throw也是一個表達式!如下所示:
fun test2() {
var i = 1
var j = 0
i = try {
i / j
1
} catch (e: Exception) {
-1
} finally {
2
}
println(i)
}
上面代碼演示了try作為表達式的案例,上面代碼由于j=0,所以在i/j的時候顯然會拋出異常,這個時候就會執行catch表達式,所以會打印-1。如果將上述代碼改成j=1,則會返回try塊中的值,即打印1。
那么finally塊中的值哪兒去了?答案是無效!在try作為表達式的時候,finally代碼塊的返回值將會被忽略。即使在finally塊中修改了一個全局的成員變量,雖然該變量的值會被改變,但是,在finally被修改后的值依然無法影響到try-catch的返回值。如下所示:
var k = 1//定義一個top-level級別的變量k
fun test2() {//測試方法test2
val i = try {
k = 0//我們在try塊中將k賦值為了0
k//k會被作為返回值返回
} finally {
k = 3//!!!我們在finally塊中修改了k的值
}
println(i)
println(k)
}
上面的代碼打印如下:
0
3
由此可證明上面我們的論斷。
下面再來看一個catch作為表達式的例子,如下所示:
fun test2(str: String?) {
//throw作為表達式
val result = str ?: throw ArithmeticException("divide by zero!")
println(result)
}
//main方法
fun main(args: Array<String>) {
test2("test")//正確,打印“test"
test2(null)//拋出ArithmeticException異常!
}
上面語句val result = str ?: throw ArithmeticException("divide by zero!")就是異常作為表達式的寫法。有沒有發現很奇怪?throw ArithmeticException明明是沒有返回值的,為什么還可以被賦值給result(即在str為null的時候)?這是怎么做到的?
實際上,這就涉及到了kotlin中一個特殊的類型Nothing,這個類型沒有任何值,只是標識代碼是“unreachable code”,即用于表示永遠不會執行到的代碼。所以,這背后,實際上是kotlin編譯器幫我們做了剩下的工作,即當發現這種類型的時候,就不在執行后面的代碼。
實際上,我們已經用過多次Nothing類型了,只不過這個Nothing類型有點特殊,我們一般無法看到,但是通過下面測試,我們可以看出一二:
fun main(args: Array<String>) {
val result = null
val result = null
}
上面代碼顯然是錯誤的,因為我們定義了兩個同名變量,但是這里不是關注這些,而是關注此時編譯器給我們的一些提示:
Conflicting declarations : val result : Nothing?,val result : Nothing?
從上面的提示可以看出,編譯器實際上將result定義為了Nothing?類型。確實是這樣的,如果我們在聲明一個變量的時候,沒有顯示指定其具體類型,并將其值初始化為了null,這個時候,kotlin就會自動推斷該變量類型為Nothing?。
Nothing?顯然表示該變量值可以為null,那么如果不允許為null,其又有什么使用場景?來看個例子:
//定義了一個Person類,有個name屬性,可為null
class Person(name: String?) {
var name: String? = name
}
//測試方法,注意我們使用了Nothing作為其返回值,
//如果不使用Nothing的話,則默認返回Unit
fun test(): Nothing {
throw IllegalArgumentException("person name can not be null!")
}
//main方法,測試方法
fun main(args: Array<String>) {
var result = Person("張三").name ?: test()
println(result)//打印'張三'
result = Person(null).name ?: test()//拋出IllegalArgumentException異常
println(result)
}
上面演示了Nothing類型的一種使用場景,其實和作為語句時基本類似。我們都知道,如果沒有指定方法的返回值,則編譯器會默認返回Unit,那么如果沒有指定test方法返回值為Nothing的話,上述代碼會執行嗎?
答案是會的,而且兩種寫法的背后機制基本一致,但還是有些許差別,具體體現在test方法的返回值以及調用處,這個可以通過對比字節碼來二者的不同,先來看下test方法生成的字節碼:
//沒有指定test方法返回值為Nothing
public final static test()V
L0
LINENUMBER 18 L0
NEW java/lang/IllegalArgumentException
DUP
LDC "divide by zero!"
INVOKESPECIAL java/lang/IllegalArgumentException.<init> (Ljava/lang/String;)V
CHECKCAST java/lang/Throwable
ATHROW
L1
MAXSTACK = 3
MAXLOCALS = 0
//指定其返回值為Nothing
public final static test()Ljava/lang/Void;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 18 L0
NEW java/lang/IllegalArgumentException
DUP
LDC "divide by zero!"
INVOKESPECIAL java/lang/IllegalArgumentException.<init> (Ljava/lang/String;)V
CHECKCAST java/lang/Throwable
ATHROW
L1
MAXSTACK = 3
MAXLOCALS = 0
通過對比,我們發現,使用Nothing的test方法,其返回值會被編譯成java.lang.void類型,而Unit則不會。其他則沒有任何差別。
再來看test方法的調用,如下所示:
//指定了返回值為Nothing
NEW Person
DUP
ACONST_NULL
INVOKESPECIAL Person.<init> (Ljava/lang/String;)V
INVOKEVIRTUAL Person.getName ()Ljava/lang/String;
DUP
IFNULL L8
GOTO L9
L8
POP
INVOKESTATIC MainKt.test ()Ljava/lang/Void;//調用test方法
ACONST_NULL
ATHROW//直接throw異常
L9
ASTORE 1
L10
//返回值沒有指定Nothing
LINENUMBER 13 L7
NEW Person
DUP
ACONST_NULL
INVOKESPECIAL Person.<init> (Ljava/lang/String;)V
INVOKEVIRTUAL Person.getName ()Ljava/lang/String;
DUP
IFNULL L8
GOTO L9
L8
POP
INVOKESTATIC MainKt.test ()V//調用test方法
GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;//這里返回了Unit實例
L9
ASTORE 1
通過上面幾處注釋,我們發現,指定返回類型為Nothing的test方法,其字節碼指令最后會直接throw一個異常,也就是上面我們代碼中寫的 throw IllegalArgumentException("divide by zero!")異常。但是沒有指定返回類型為Nothing的test方法,僅僅是按照正常方法調用,最后返回了默認的Unit實例。
換句話說,使用Nothing類型作為方法返回值類型的時候,其被編譯的字節碼流程中已經被嵌入了throw異常的指令,是按照代碼的正常執行邏輯,一步步執行的;而不使用Nothing類型作為方法返回值類型時,字節碼層次并沒有在其流程中插入拋出異常指令,顯然,此時,如果出現異常,則會在運行時拋出。
在語法上,指定返回類型是Nothing和不指定返回類型為Nothing的最大區別是:當指定返回類型是Nothing時,方法是不能寫任何返回值的;而不指定的時候,則可以顯示寫返回值為Unit,當然也可以省略。
那么問題來了,我們既然指定了方法的返回值是Nothing,而前面又說我們不能寫任何返回值,這不是自相矛盾嗎?
確實如此,既然方法有返回值,同時該返回值類型也不是Unit,所以,我們理所當然要顯示指定其返回值類型,但使用Nothing修飾的方法又不能有返回值,貌似是個無法解開的閉環問題?想來想去,就只還有個方法:我們能不能顯示返回Nothing實例?
很遺憾,答案是不能,因為Nothing的構造方法是私有的:
public class Nothing private constructor()
看到這里一定會絕望的,但實際上這一切kotlin已經給我們制定好了規則:對于Nothing這個類型,在kotlin的世界里面,表示一個“永遠不存在的值”,Nothing類型沒有任何實例。當Nothing修飾方法時,表示該方法永遠沒有返回值。那么怎么做,才能讓方法沒有返回值?那就是該方法必須拋出異常。
至此,kotlin異常相關的機制闡述完畢。