要抓還是要拋?


假設今天你受命開發一個程式庫,其中有個功能是讀取純文字檔案,並以字串傳回所有檔案中所有文字,你也許會這麼撰寫:

public class FileUtil {
    public static String readFile(String name) {
        StringBuilder builder = new StringBuilder();
        try {
            Scanner scanner = new Scanner(new FileInputStream(name));
            while(scanner.hasNext()) {
                builder.append(scanner.nextLine());
                builder.append('\n');
            }
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        }
        return builder.toString();
    }
}

雖然還沒正式介紹到Java中如何存取檔案,不過Scanner建構時可以給予InputStream實例,而FileInputStream可指定檔名來開啟與讀取檔案內容,是InputStream的子類別,因此可作為Scanner建構之用。由於建構FileInputStream時,API設計者聲明方法實作中會拋出FileNotFoundException,根據目前你學到的例外處理語法,於是你捕捉FileNotFoundException並在主控台中顯示錯誤訊息。

主控台?等一下!老闆有說這個程式庫會用在文字模式中嗎?如果這個程式庫是用在Web網站上,發生錯誤時顯示在主控台上,Web使用者怎麼會看得到?你開發的是程式庫,例外發生時如何處理,是程式庫使用者才知道,直接在catch中寫死處理例外或輸出錯誤訊息的方式,並不符合需求。

如果方法設計流程中可能引發例外,而你設計時並沒有充足的資訊知道該如何處理(例如不知道程式庫會用在什麼環境),那麼可以拋出例外,讓呼叫方法的客戶端來處理。例如:

public class FileUtil {
    public static String readFile(String name)
                                   throws FileNotFoundException {
        StringBuilder builder = new StringBuilder();
        Scanner scanner = new Scanner(new FileInputStream(name));
        while(scanner.hasNext()) {
            builder.append(scanner.nextLine());
            builder.append('\n');
        }
        return builder.toString();
    }
}

操作物件的過程中如果會拋出受檢例外,但目前環境資訊不足以處理例外,所以無法使用trycatch處理時,可由方法的客戶端依當時呼叫的環境資訊進行處理。為了告訴編譯器這件事實,必須在方法上使用throws宣告此方法會拋出的例外類型或父類型,編譯器才會讓你通過編譯。

拋出受檢例外,表示你認為呼叫方法的客戶端有能力且應該處理例外,throws宣告部份,會是API操作介面的一部份,客戶端不用察看原始碼,從API文件上就能直接得知,該方法可能拋出哪些例外。

如果你認為客戶端呼叫方法的時機不當引發了某個錯誤,希望客戶端準備好前置條件,再來呼叫方法,這時可以拋出非受檢例外讓客戶端得知此情況,如果是非受檢例外,編譯器不會要求明確使用trycatch或在方法上使用throws宣告,因為Java的設計上認為,非受檢例外是程式設計不當引發的臭蟲,例外應自動往外傳播,不應使用trycatch處理,而應改善程式邏輯來避免引發錯誤。

實際上在例外發生時,可使用trycatch處理當時環境可進行的例外處理,當時環境下無法決定如何處理的部份,可以拋出由呼叫方法的客戶端處理。如果想先處理部份事項再拋出,可以如下:

package cc.openhome;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class FileUtil {
    public static String readFile(String name) throws FileNotFoundException {
        StringBuilder builder = new StringBuilder();
        try {
            Scanner scanner = new Scanner(new FileInputStream(name));
            while(scanner.hasNext()) {
                builder.append(scanner.nextLine());
                builder.append('\n');
            }
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            throw ex;
        }
        return builder.toString();
    }
}

範例在catch區塊進行完部份錯誤處理之後,可以使用throw(注意不是throws)將例外再拋出,實際上,你可以在任何流程中拋出例外,不一定要在catch區塊中,在流程中拋出例外,就直接跳離原有的流程,可以拋出受檢或非受檢例外,記得!如果拋出的是受檢例外,表示你認為客戶端有能力且應處理例外,此時必須在方法上使用throws宣告,如果拋出的例外是非受檢例外,表示你認為客戶端呼叫方法的時機出錯了,拋出例外是要求客戶端修正這個臭蟲再來呼叫方法,此時也就不用使用throws宣告。

如果原先有個方法實作是這樣的:

public static void doSome(String arg)
            throws FileNotFoundException, EOFException {
    try {
        if("one".equals(arg)) {
            throw new FileNotFoundException();
        } else {
            throw new EOFException();
        }
    } catch(FileNotFoundException ex) {
        ex.printStackTrace();
        throw ex;
    } catch(EOFException ex) {
        ex.printStackTrace();
        throw ex;
    }   
}

你發現到FileNotFoundExceptionEOFException都是一種IOException,而且catch後都作相同的事,於是想要使用IOExceptioncatch這兩種類型的例外,以下的寫法在JDK6之前都會出錯:

多重捕捉時也得注意例外繼承架構


在這個程式片段中,雖然實際上捕捉到的一定是FileNotFoundExceptionEOFException實例,方法上也使用throws予以宣告了,但JDK6之前的編譯器沒那麼聰明,因而出現編譯錯誤。在JDK7中,編譯器對於重新拋出的例外型態可以更精準判斷(more-precise-rethrow),因此上面的程式片段,在JDK7中不會再出現編譯錯誤。

如果使用繼承時,父類別某個方法宣告throws某些例外,子類別重新定義該方法時可以:
  • 不宣告throws任何例外
  • throws父類別該方法中宣告的某些例外
  • throws父類別該方法中宣告例外之子類別
但是不可以:
  • throws父類別方法中未宣告的其它例外
  • throws父類別方法中宣告例外之父類別