---
# System prepended metadata

title: 大型程式
tags: [java]

---

# 大型程式

###### 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 之類的判斷式來使用，不應被當做程式執行流程的一部份；也就是通常只會用在開發中的程式除錯用，在發佈的版本中，不應該使用斷言。