假設今天你受命開發一個程式庫,其中有個功能是讀取純文字檔案,並以字串傳回所有檔案中所有文字,你也許會這麼撰寫:
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();
}
}
操作物件的過程中如果會拋出受檢例外,但目前環境資訊不足以處理例外,所以無法使用try
、catch
處理時,可由方法的客戶端依當時呼叫的環境資訊進行處理。為了告訴編譯器這件事實,必須在方法上使用throws
宣告此方法會拋出的例外類型或父類型,編譯器才會讓你通過編譯。
拋出受檢例外,表示你認為呼叫方法的客戶端有能力且應該處理例外,throws
宣告部份,會是API操作介面的一部份,客戶端不用察看原始碼,從API文件上就能直接得知,該方法可能拋出哪些例外。
如果你認為客戶端呼叫方法的時機不當引發了某個錯誤,希望客戶端準備好前置條件,再來呼叫方法,這時可以拋出非受檢例外讓客戶端得知此情況,如果是非受檢例外,編譯器不會要求明確使用try
、catch
或在方法上使用throws
宣告,因為Java的設計上認為,非受檢例外是程式設計不當引發的臭蟲,例外應自動往外傳播,不應使用try
、catch
處理,而應改善程式邏輯來避免引發錯誤。
實際上在例外發生時,可使用try
、catch
處理當時環境可進行的例外處理,當時環境下無法決定如何處理的部份,可以拋出由呼叫方法的客戶端處理。如果想先處理部份事項再拋出,可以如下:
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;
}
}
你發現到FileNotFoundException
與EOFException
都是一種IOException
,而且catch
後都作相同的事,於是想要使用IOException
來catch
這兩種類型的例外,以下的寫法在JDK6之前都會出錯:
在這個程式片段中,雖然實際上捕捉到的一定是
FileNotFoundException
與EOFException
實例,方法上也使用throws
予以宣告了,但JDK6之前的編譯器沒那麼聰明,因而出現編譯錯誤。在JDK7中,編譯器對於重新拋出的例外型態可以更精準判斷(more-precise-rethrow),因此上面的程式片段,在JDK7中不會再出現編譯錯誤。如果使用繼承時,父類別某個方法宣告
throws
某些例外,子類別重新定義該方法時可以:- 不宣告
throws
任何例外 - 可
throws
父類別該方法中宣告的某些例外 - 可
throws
父類別該方法中宣告例外之子類別
throws
父類別方法中未宣告的其它例外throws
父類別方法中宣告例外之父類別