這章要來為大家介紹例外處理(Exception Handing)
,但其實在介紹例外處理之前,想先和大家介紹錯誤(Error)
與例外(Exception)
的差別,避免大家搞混兩者。
錯誤(Error)
通常是指程式在正常運行之下,可能受到硬體資源影響所導致的錯誤,進而導致Java虛擬機(JVM)處於一種不正常且不可恢復的狀態,例如記憶體溢位 OutOfMemoryError
。而像 Error 這類型的錯誤在Java 或 Kotlin 都是使用 Error
類別表示,不同錯誤類別代表著不同錯誤,且每個錯誤類別都是繼承自 Error
類別,例如StackOverflowError
、OutOfMemoryError
。
例外(Exception)
通常是指程式的語法錯誤或語意錯誤,即程式在編譯時期(Compile Time)和執行時期(Execute Time)出現的錯誤,此類錯誤我們可以稱為例外(Exception),在Java 或 Kotlin 都是使用 Exception
類別來表示,而 Exception 又可分為 Checked Exception
(已檢查例外)與 Unchecked Exception
(未檢查例外),Checked Exception
是指程式碼必須明確配合例外檢查方法(例如 try/catch )進行檢查,否則編譯時期會無法編譯;Unchecked Exception
是指開發者可以自行判斷程式碼是否需要加上例外檢查,在編譯時期不會強制要求。
而在 Java 與 Kotlin 中, Error
與 Exception
都是繼承自 Throwable
類別,也只有 Throwable
類別才能夠拋出(throw)錯誤,類別關係可參考下圖:
圖片引用自 instanceofjava 文章
接下來,我們來認識什麼是例外處理(Exception Handing ),它通常是指我們針對程式執行時所發生的例外狀況進行有效處理的方法,若我們能夠妥善處理例外情形,則可以提高程式的強健度,讓程式即便發生錯誤也能正常執行,不會因為某一個錯誤而導致整個軟體崩潰,保持良好的軟體使用體驗。
而 Kotlin 也是一門編譯型程式語言,即程式碼會先編譯成機器語言,再由編譯器進行執行。在編譯階段(Compiler Time)時,編譯器會檢查程式碼是否符合特定要求,確定沒問題後再進行編譯,例如在變數章節我們所提到的空值檢查機制,編譯器會幫我們判斷是否將 null 值指派給非空類型。
在文章開頭,我們有介紹什麼是例外(Exception),例外又可分為兩種類型,即 Checked Exception
(已檢查例外)與 Unchecked Exception
(未檢查例外),在 Java 世界中,兩種例外類型都有支援,但在 Kotlin 世界中,本身不支援 Checked Exception 類型(可參考官方文件說明),所以當我們撰寫的程式碼有可能拋出 Exception,在編譯時期都會直接通過,在執行時期才會發現。
可能有些人會好奇,Kotlin 取消 Checked Exception
類型會不會容易造成問題發生,Kotlin 官方其實也有做出回應並引用 Bruce Eckel 的論述:
Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality.
當初 Kotlin 在設計考量時也是參考過去 Java 的開發經驗,設計讓部份錯誤檢查可以從執行時期提前到編譯時期發現,能夠讓我們更早發現程式問題,防患於未然,這也是選用 Kotlin 設計程式的優勢之一,假設保留Checked Exception
類型的話,當開發者在大型軟體專案中使用過多時,大部份開發上只會造成程式碼可讀性變差與程式碼品質下降。
上面我們介紹了例外處理的基本概念,我們再來介紹 Kotlin 處理例外狀況的方法,一般程式語言都是利用 try..catch..finally
來處理例外狀況,而 Kotlin 也不例外,使用方法與範例如下:
使用方法說明:
try {
// 預期可能會發生錯誤的程式碼
}
catch (exception: SomeException) {
// 當發生錯誤時,則執行這裡的程式碼
}
finally {
// 最後執行的程式碼區塊,此區塊可以忽略
}
我們利用一個簡單範例,當運算式為 數字 1 除 數字 0時,程式會出現 ArithmeticException 類型錯誤:
fun main(){
try {
println("1.執行程式")
val data = 1 / 0
} catch (exception: ArithmeticException) {
println("2.發生錯誤")
println(exception)
} finally {
println("3.最後執行的程式碼")
}
}
// 此程式會輸出以下結果
// 1.執行程式
// 2.發生錯誤
// java.lang.ArithmeticException: / by zero
// 3.最後執行的程式碼
Kotlin 也可以允許開發者主動拋例外物件,會由 throw
運算子所觸發,拋出異常就代表程式若要繼續執行,必須先解決這個問題才能夠正常繼續執行,例如以下範例,我們有一個函數是判斷數值是否符合正整數,如果不符合則主動拋出例外錯誤:
我們也可以自定義一個例外錯誤類別,宣告一個類別並繼承 Exception
類別即可,自定義例外錯誤類別可以讓我們在除錯時更清楚拋出的資訊是屬於哪一種問題,例如以下範例:
上面是我們一般會對於處理例外狀況所撰寫的程式方法,但實際上在這三個區塊會有各自要處理的責任,這邊稍微簡單說明:
try
區塊除了提到必須負責實作業務需求之外,也必須負責準備錯誤發生時的狀態回復方法,建立將程式狀態回復至發生錯誤之前的方法
catch
區塊除了回報錯誤狀況以外,其實還要身兼錯誤的對應處理或重試其他替代方案,例如上面範例,當使用者輸入為錯誤數值時,是否可以藉由 Catch 提供錯誤提示給予使用者,給予使用者選擇重試或取消此計算功能
finally
區塊則是擔任釋放資源與回報發生錯誤例外的角色,假設我們執行的程式碼是與資料庫溝通的程式,在 finally 則是必須釋放資料庫連線。
此篇文章介紹了例外處理的基本概念與使用方法,其實例外處理要考量的地方還有許多,可能沒辦法利用一篇文章進行詳細說明,後續在 Spring Boot 章節會再補充例外處理的方法,而在實務開發上,團隊通常也都會有一份開發規定進行對應處理,方便每個人在開發專案時有一定的共識。