# 大型程式 ###### tags: `java` ## 目錄 - 套件管理 - [例外](#例外) - [例外處理](#例外處理) - [例外種類](#例外種類) - [拋出例外](#拋出例外) - [例外的繼承架構](#例外地繼承架構) - [斷言 - assertion](#斷言) # 例外 當程式發生預期外的錯誤或臭蟲(Bug),可能會造成程式無法正常執行或錯誤的結果,,面對程式中各種錯誤,Java 提供了「例外處理(Exception Handling)」機制來處理可能發生的錯誤,「例外(Exception)」在 Java 中為一個錯誤的實體,編譯器在編譯的時候會幫忙檢查一些可能產生例外(Checked exception)的狀況,並強制要求處理,而對於另一種「執行時期例外(Runtime exception)」,則可以選擇嘗試捕捉例外並將程式回復至正常狀況或是不處理。 ------ ## 例外處理 例外處理(Exception Handling);下面程式碼的樣子可能常常出現在自己的程式當中,為了避免寸去超過陣列長度,會用一些判斷式的手法來檢查並避免,例如:: ``` if(args.length == 0) { // 檢查陣列資料長度,沒有資料的話執行這裡 } else { // 有資料就執行這裡 } ``` 利用條件判斷式來避免錯誤的發生,這樣的檢查方式在一些程式語言中經常出現,用這種方式處理錯誤的邏輯與處理業務的邏輯都混在一起,如果有更多的錯誤狀況必須進行檢查的話,程式會更加冗長並難以閱讀,且由於使用了一些判斷式,即使發生機率低的錯誤,也都進行判斷檢查,這在程式的執行效能會受到一些程度上的影響。 Java 的例外處理機制可以協助避開或是處理程式可能發生的錯誤,「例外」(Exception)在 Java 中代表一個錯誤的實體物件,在特定錯誤發生時會丟出特定的例外物件,例外有兩大類,預期中可能發生的例外,編譯器會強制必須要處理,執行時所發生的執行時期例外,則可以選擇處理或是不處理。 想捕捉例外,可以使用 "try"、"catch"、"finally" 三個關鍵字組合,語法如下: ``` try { // 陳述句區塊 } catch(例外型態 名稱) { // 例外處理區塊 } finally { // 最後一定會處執行的區塊 } ``` **解說** 1. 一個 "try" 語法所包括的區塊,必須有對應的 "catch" 區塊或是 "finally" 區塊。 2. "try" 區塊可以搭配多個 "catch" 區塊,如果有設定 "catch" 區塊,則 "finally" 區塊可有可無,如果沒有定義 "catch" 區塊,則一定要有 "finally" 區塊。 3. 「finally」區塊表示「try」區塊的陳述句不管有沒有完成執行(可能產生例外了就不會全部執行完),最後都ㄧ定會被執行到。 例如: ``` public class App { public static void main(String[] args) { try { System.out.printf("執行 %s 功能%n", args[0]); } catch(ArrayIndexOutOfBoundsException e) { System.out.println("沒有引數"); e.printStackTrace(); } } } ``` 如果在執行程式時沒有指定引數,那麼args陣列的長度是 0,程式中嘗試從 args[0] 取得引數時就會發生錯誤而攢生例外,例外物件名稱為ArrayIndexOutOfBoundsException,這個實例會在被對應的「catch」所捕捉,在範例 中被捕捉的例外指定給 e 名稱來參考,有點類似方法的參數宣告,例外被捕捉後會執行對應的「catch」區塊,在範例中是顯示提示訊息,並使用 printStackTrace() 會顯示完整的例外訊息,沒有指定引數時的執行結果如下: ``` # java App 沒有指定引數 java.lang.ArrayIndexOutOfBoundsException: 0 at App.main(App.java:4) ``` 這裡並沒有使用條件判斷式來檢查陣列長度,也就是沒有使用 if 判斷式,例外處理只有在錯誤真正發生,也就是丟出例外時才處理,所以與使用 if 判斷式每次都要進行檢查動作相比,效率上會好一些,要注意的是,例外處理最好只用於錯誤處理,而不應是用於程式業務邏輯的一部份,因為例外的產生要消耗資源,例如以下應用例外處理的方式就不適當: ``` while(true) { try { System.out.println(args[i]); i++; } catch(ArrayIndexOutOfBoundsException e) { break; } } ``` 循序取出陣列值時,最後一定會到達陣列的邊界,檢查陣列邊界是必要的動作,是程式業務邏輯的一部份,而不是錯誤處理邏輯的一部份,應該使用的是 for 迴圈而不是依賴例外處理,下面的方式才正確: ``` for(int i = 0; i < args.length; i++) { System.out.println(args[i]); } ``` ## 例外種類 分為兩大類: - 受檢例外(Checked Exception) - 執行時期例外(Runtime Exception) #### 受檢例外(Checked Exception) 在某些情況下例外的發生是可預期的,例如使用輸入輸出功能時,可能會由於硬體環境問題,而使得程式無法正常從硬體取得輸入或進行輸出,這種錯誤是可預期發生的,這類的例外稱之為「受檢例外」(Checked Exception),對於受檢例外編譯器會要求程式一定要進行處理,例如在使用 java.io.BufferedReader 的 readLine() 方法取得使用者輸入時,編譯器會要求在程式中明確告知如何處理 java.io.IOException,例如: ``` import java.io.*; public class App { public static void main(String[] args) { try { BufferedReader buf = new BufferedReader( new InputStreamReader(System.in)); System.out.print("請輸入整數: "); int input = Integer.parseInt(buf.readLine()); System.out.println("input x 10 = " + (input*10)); } catch(IOException e) { // checked exception System.out.println("I/O錯誤"); } catch(NumberFormatException e) { // runtime exception System.out.println("輸入必須為整數"); } } } ``` IOException 是受檢例外,是可預期會發生的例外,編譯器要求您必須處理,如果您不在程式中處理的話,例如將 IOException 的 "catch" 區塊拿掉,編譯器會回報錯誤訊息: ``` App.java:9: unreported exception java.io.IOException; must be caught or declared to be thrown ``` 這個範例會從使用者輸入取得一個整數值,由 BufferedReader 物件所讀取到的輸入是個字串,當使用 Integer 類別的 parseInt() 方法試著解析該字串為整數,如果無法解析,則會發生錯誤並丟出一個 NumberFormatException 例外物件,當這個例外丟出後,程式會離開目前執行的位置,而如果設定的 "catch" 有捕捉這個例外,則會執行對應區塊中的陳述句,注意當例外一但丟出,就不會再回到例外的丟出點並繼續執行了了,而會跳出try區塊外再往下繼續執行,如果有定義finally區塊,則會進行該區塊的執行。 像 NumberFortmatException 例外是「執行時期例外」(Runtime exception),也就是例外是發生在程式執行期間,並不一定可預期它的發生,編譯器不要求您一定要處理,對於==執行時期例外若沒有處理,則例外會一直往外丟,最後由 JVM來處理例外,JVM 所作的就是顯示例外的堆疊訊息,然後結束程式==。 如果 try...catch 後設定有 "finally" 區塊,則無論例外是否有發生,都一定會執行 "finally" 區塊。 ## 拋出例外 - throw:自行產生例外 - throws:向外拋出例外 當程式發生錯誤而無法處理的時候,JVM會丟出對應的例外物件,但我們也可以自行丟出例外,例如在捕捉例外並處理結束後,再將例外丟出,讓下一層例外處理區塊來捕捉;另一個狀況是重新包裝例外,將捕捉到的例外以自己定義的例外物件加以包裝丟出。若想要自行丟出例外,可以使用「throw" 關鍵字」,並生成指定的例外物件,例如: ``` throw new NullPointerException(); ``` 當對著一個null物件呼叫方法時,就會產生NullPointerException例外,例如: ``` public class App { public static void main(String[] args) { String nullStr = null; System.out.println(nullStr.length()); } } ``` 因為nullStr為一個null物件,所以執行結果為: ``` Exception in thread "main" java.lang.NullPointerException at App.main(App.java:6) ``` #### 向外拋例外 如果在方法中會有例外的發生,但並不想在方法中直接處理,而想要由呼叫方法的呼叫者來處理,則可以使用 「throws」關鍵字來宣告這個方法將會丟出例外,例如 java.ioBufferedReader 的 readLine() 方法會丟出 java.io.IOException例外,但是在呼叫br.readLine()時不打算處理,而是希望上一層的呼叫者處理,則可以再方法後面加上throws來向外拋出,如果已經是main方法,向外拋出後會由JVM來處理,也就是相當於預設處理方式。 例如: ``` private void someMethod() throws ArrayIndexOutOfBoundsException, ArithmeticException { // 則這個方法內產生的ArrayIndexOutOfBoundsException和ArithmeticException 例外都會被往外拋出 } ``` > **補充** > > 如果打算往外丟出超過一個例外時,可以使用逗點分隔;當方法上使用 "throws" 宣告丟出例外時,意味著呼叫該方法的呼叫者必須處理這些例外。 #### **範例 10.6 ThrowsDemo.java** ``` public class App { public static void main(String[] args) { try { throwsTest(); } catch(NullPointException e) { System.out.println("捕捉到NullPointException"); } } private static void throwsTest() throws NullPointException { String nullStr = null; System.out.println(nullStr.length()); } } ``` 執行結果: ``` 捕捉到NullPointException ``` 簡單的說,要不就在方法中直接處理例外,要不就在方法上宣告該方法會丟回例外,由呼叫者來處理例外。 ## 例外的繼承架構 例外可分為兩大類: 1. 嚴重錯誤:例如硬體錯誤或記憶體不足等問題,與此相關的類別是位於 java.lang 下的 Error 類別及其子類別,對於這類的錯誤通常程式是無力自行回復。 2. 非嚴重的錯誤:代表可以處理的狀況,例如使用者輸入了不合格式的資料,這種錯誤程式有機會回復至正常運作狀況,與這類錯誤相關的類別是位於 java.lang 下的 Exception 類別及其子類別。 Error 類別與 Exception 類別都繼承自 Throwable 類別,Throwable 類別擁有幾個取得相關例外訊息的方法。 | 方法名稱 | 說明 | | --------------------- | ---------------------------- | | getLocalizedMessage() | 取得例外物件的區域化訊息描述 | | getMessage() | 取得例外物件的訊息描述 | | printStackTrace() | 顯示例外的堆疊訊息 | | toString() | 取得例外的簡單訊息描述 | > **補充** > > printStackTrace()方法在追蹤例外發生的源頭時相當的有用,例如:在A 方法中呼叫了 B 方法,而 B 方法中呼叫了 C 方法,C 方法產生了例外,則在處理這個例外時呼叫 printStackTrace() 可以得知整個方法呼叫的過程,由此得知例外是如何被層層丟出的。 例外通常都是衍生自 Exception 類別,該類別又分為兩類: 1. 「受檢例外」(Checked exception),例如 ClassNotFoundException(嘗試載入類別時失敗所引發,例如類別檔案不存在)、InterruptedException(執行緒非執行中而嘗試中斷所引發的例外)等。 2. 「執行時期例外」(Runtime exception),例如 ArithmeticException、ArrayIndexOutOfBoundsException 等。 以下列出一些重要的例外繼承架構: ``` Throwable Error(嚴重的系統錯誤) LinkageError ThreadDeath VirtualMachineError AssertError .... Exception(非嚴重的錯誤) InterruptException ... ReflectiveOperationException ClassNotFoundException ...      IOException      FileNotFoundexception      ...      RuntimeException NullPointerException ArithmeticException ClassCastException ... ``` 1. Exception 下非 RuntimeException 衍生之例外類別如果有引發的可能,則一定要在程式中明確的指定處理才可以通過編譯,因為這些例外是可預期的,編譯器會要求一定要處理,所以才稱之為「受檢例外」(Checked exception),例如使用到 BufferedReader 的 readLine() 時,有可能引發的IOException 這個受檢例外,不是在 try...catch 中處理,就是在方法上使用 "throws" 來往上拋出給呼叫者。 2. 屬於 RuntimeException 衍生出來的類別是「執行時期例外」(Runtime exception),是在執行時期會發生的例外,這個例外不一定會發生,需看程式邏輯本身,所以不會被編譯器強制使用 try...catch 或是在方法上使用 "throws" 來處理,所以稱之為「非受檢例外」(Unchecked exception);例如在使用陣列時,並不一定要處理 ArrayIndexOutOfBoundsException 例外,因為只要程式邏輯寫的正確,這個例外就不會發生。 了解例外處理的繼承架構是必要的,如果在捕捉例外物件時,該例外的父類別被先捕捉了,則例外的子類別 "catch" 區塊永遠不會被執行,編譯器也檢查這個錯誤,例如: ``` import java.io.*; public class App { public static void main(String[] args) { try { throw new NullPointerException("例外測試"); } catch(Exception e) { System.out.println(e.toString()); } catch(NullPointerException e) { System.out.println(e.toString()); } } } ``` 因為 Exception 是 ArithmeticException 的父類別,所以 ArithmeticException 的實例會先被 Exception 的 "catch" 區塊捕捉到,範例 10.7 在編譯時將會產生以下的錯誤訊息: ``` Unreachable catch block for NullPointerException. It is already handled by the catch block for Exception ``` 要完成這個程式的編譯,順序需要調整一下,必須先捕捉子類別,例如: ``` import java.io.*; public class App { public static void main(String[] args) { try { throw new NullPointerException("例外測試"); } catch(Exception e) { System.out.println(e.toString()); } catch(NullPointerException e) { System.out.println(e.toString()); } } } ``` 在開發程式階段時,常常會將Exception 例外物件的捕捉寫在最後面,以便捕捉到所有沒有考慮到的例外(有時候則是開發者偷懶,只有捕捉Exception),會對除錯有幫助。 如果要自訂自已的例外類別,可以繼承 Exception 類別而不是 Error 類別,Error 是屬於嚴重的系統錯誤,程式通常無力從這類的錯誤中回復,所以不需要去處理它。 ## 斷言 斷言(assertion);例外是程式中非預期的錯誤,例外處理是在這些錯誤發生時所採取的措施。 有些時候,預期程式中應該會處於何種狀態或運算後產生的值必須是多少,則稱為一種斷言。 斷言會有兩種結果:成立或不成立。當預期結果與符合實際執行結果時,斷言成立,否則不成立。 斷言有兩種使用的語法: ``` assert boolean_expression; assert boolean_expression : detail_expression; ``` 1. boolean_expression 如果為 true,則什麼事都不會發生,如果為 false,則會發生 java.lang.AssertionError例外。 2. 產生java.lang.AssertionError時,若採取的是第二個語法,則會將 detail_expression 的結果顯示出來,如果當中是個物件,則呼叫它的 toString() 顯示文字描述結果。 斷言通常是一個程式內部不變量(Internal invarant)的判斷,例如在某個時間點上,或某個狀況發生時,判斷某個變數一定是要某個值,例如: ``` public class App { public static void main(String[] args) { String nullStr = null; if(nullStr == null) { System.out.println("出現空字串"); assert nullStr != null; } System.out.println("程式正常結束"); } } ``` 執行結果: ``` 出現空字串 Exception in thread "main" java.lang.AssertionError at App.main(App.java:7) ``` 另一種用法: ``` public class App { public static void main(String[] args) { String nullStr = null; assert nullStr != null : "出現空字串"; System.out.println("程式正常結束"); } } ``` 執行結果: ``` Exception in thread "main" java.lang.AssertionError: 出現空字串 at App.main(App.java:5) ``` 在正常的預期中,字串物件絕不會是null物件,所以一但執行到`if(nullStr == null)`區塊,表示發生了不應該出現的狀況,所以當assert判斷為false,就會發生java.lang.AssertionError例外並停止執行,表示要停下來檢查程式的錯誤,斷言主要的目的通常是在開發時期測試使用,程式發佈後應該將斷言關閉。 > **補充** > > 預設上執行時是不啟動斷言檢查的,如果要在執行時啟動斷言檢查,可以使用 -enableassertions 或是 -ea 引數,例如: > ``` java –ea AssertionDemo > ``` > 如果是使用Visual Studio Code開發,可以在launch.json加上`"vmArgs": "-enableassertions"`設定,例如: > ``` > { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "java", "name": "Launch Current File", "request": "launch", "mainClass": "${file}", "vmArgs": "-enableassertions" } ] } 也可以使用在流程的控制上,例如使用在switch 時: ``` switch(answer) { case "YES": ... break; case "NO": ... break; default: assert false : "不該出現的情況"; } ``` 假設您已經在 switch 中列出了所有的常數,即answer不該出現"YES", "NO"以外的字串,則如果發生 default 被執行的情況,表示程式的狀態與預期不符,此時由於 assert false,所以斷言一定會失敗並且拋出AssertionError例外。 > **提醒** > > 斷言是判定程式中的某個執行點必然是某個狀態,否則程式會被中斷,所以它不能當作像 if 之類的判斷式來使用,不應被當做程式執行流程的一部份;也就是通常只會用在開發中的程式除錯用,在發佈的版本中,不應該使用斷言。