1. 程式人生 > 程式設計 >一篇文章解決Java異常處理

一篇文章解決Java異常處理

前言

與異常相關的內容其實很早就想寫了,但由於各種原因(懶)拖到了現在。在大二開學前夜(今天是8.31)完成這篇部落格,也算完成了暑期生活的一個小心願。

以下內容大多總結自《Java核心技術 卷Ⅰ》,同時也加上了一些華東師範大學陳良育老師在《Java核心技術》Mooc中所講的內容。

一、引例

假定你希望完成一個read方法,它的作用是讀取一個檔案中的內容並進行相關處理,如果你從未學過處理異常的方法,你可能會這樣寫:

public void read(String filename)
{
 var in = new FileInputStream(filename);
 int b;
 while((b = in.read()) != -1)
 {
  ... 
 }
}

問題在於,FileInputStream的構造器要求傳入的filename是已經存在的一個檔案的名稱,如果檔名不存在,那麼程式將異常終止,可能導致使用者在執行程式期間所做的工作全部丟失,這顯然不是我們希望看到的。
正確的做法之一是增加一個異常處理機制,將控制權從產生錯誤的地方轉移到能夠處理這種錯誤的處理器中。這樣就可以儘可能的減少損失。

public void read(String filename)
{
  try
  {
   var in = new FileInputStream(filename);
   int b;
   while((b = in.read()) != -1)
   {
     ...
   }
  }
  catch(IOException exception)
  {
   ...//處理錯誤
  }
}
//裡面可能有些程式碼你暫且還不明白,不過沒關係,之後會認真講解。

二、異常的分類與型別

在Java中,每種異常都是一個類(如上例中的IOException),每個異常物件都是一個例項。

  1. 所有的異常都是由Throwable類繼承而來。
  2. Throwable類的下一層有兩個分支:Error類和Exception類
  3. Exception類又分解為RuntimeException(程式設計錯誤導致的異常)與IOException(其他異常)

Error類:描述Java執行時系統內部錯誤和資源耗盡錯誤。 RuntimeException類:程式設計錯誤。例如陣列越界訪問、錯誤的強制型別轉換、訪問null指標等。
IOException類:其他異常。例如開啟不存在的檔案等等。

對於Error異常,我們無能為力;而對於RuntimeException異常,我們要做的是預防而非處理,譬如在程式中寫好檢測陣列下標是否越界的程式碼、在使用變數前檢測它是否為null;我們重點處理的物件是IOException異常。

鑑於上述特性,Error與RuntimeException異常在Java語言規範中被稱為非檢查型異常;IOException稱為檢查型異常。編譯器只會幫助你檢查是否為所有的檢查型異常提供了異常處理器。

一篇文章解決Java異常處理

三、處理檢查型異常之方法一:宣告與丟擲

如果你知道一個方法會產生某種(或多種)檢查型異常,但又不想處理這個異常(或無法處理),可以選擇僅宣告該類異常,將異常的處理交由呼叫這個方法的人。

具體的操作方式為:在方法名稱後面加上 throws + 異常型別,程式中如果真的遇到了這種異常,則 throw + 異常類的物件丟擲異常。

String readData(Scanner in) throws EOFException
{
  ...
  while(...)
  {
   if(!in.hasNext())
   {
     if(n < len)//遇見EOFException異常
     {
      throw new EOFException();//丟擲異常
     }
     ...
   }
   return s;
  }
}

宣告異常時請注意:

  • 如果一個方法可能丟擲多個檢查型異常,則必須列出所有的檢查型的異常類。(否則編譯器會發出一個錯誤訊息)
public void read(String filename) throws FileNotFoundException, EOFException
  • 如果子類覆蓋了父類中的一個方法,則子類中該方法對異常的聲明範圍不能超過父類(即子類中覆蓋的方法要麼丟擲更具體的異常,要麼不丟擲異常)。
  • 如果一個方法宣告它會丟擲一個異常,那麼這個方法丟擲的異常可能屬於這個類,也可能屬於這個類的子類。

丟擲異常時請注意:

  • 如果一個方法呼叫了丟擲檢查型異常的方法,這個方法要麼處理這個異常(怎麼處理標題五會詳細介紹),要麼繼續傳遞這個異常(即繼續throws)。
  • 如果一個方法是覆蓋了超類中的方法,並且在超類中這個方法沒有丟擲異常,那麼你別無選擇,必須處理所有可能發生的檢查型異常(因為子類中的覆蓋方法丟擲範圍不得超過父類)。

四、定義自己的異常類

標題三中的處理方法雖然簡便,但有一個很大的缺點:你必須找到一個合適的可能觸發的異常類別,才能將其丟擲。於是我們想,可不可以自己定義一個異常類別,這樣即使我們找不到合適的異常類,也可以將其丟擲?

通常讓自定義的類繼承於Exception類或者IOException類,並且按照習慣,任何異常類都至少需要包含兩個構造器,一個是預設構造器,另一個是包含有詳細描述資訊的構造器(方便除錯)。
舉例:

class FileFormatException extends IOException//自定義異常類
{
  public FileFormatException(){}
  public FileFormarException(String gripe)
  {
   super(gripe);
  }
}

String readData(BufferedReader in) throw FileFormatException//宣告自定義的異常
{
  ...
  while(...)
  {
   if(ch == -1) 
   {
     if(n < len) throw new FileFormatException();//丟擲自定義的異常
   }
  }
}

五、處理檢查型異常之方法二:try-catch-finally捕獲異常

現在讓我們回到引例上來:

public void read(String filename)
{
  try
  {
   var in = new FileInputStream(filename);
   int b;
   while((b = in.read()) != -1)
   {
     ...
   }
  }
  catch(IOException exception)
  {
   ...
  }
}

小知識:“如果發生了某個異常,但沒有被捕獲,程式就會終止,並在控制檯上列印一個訊息,其中包括這個異常的型別和一個堆疊軌跡。”

引例中完成的是一個最簡單的 try-catch 語句,現在我們來分析一下不同情況下程式相應的執行情況:

  • 如果 try 中出現了一個異常,且該異常被成功捕獲(在上例中即異常型別剛好為catch中的IOException型別)。則程式會跳過 try 中其餘程式碼,接著執行 catch 子句中的程式碼。
  • 如果 try 中出現了一個異常,且該異常未被成功捕獲(即異常型別與catch中的異常型別不符)。則程式會立即退出。
  • 如果 try 中未出現任何異常,將不會執行 catch 中的語句。

多 catch 捕獲多個異常:

在一個try語句塊中可以捕獲多個異常型別,並對每個異常型別使用一個單獨的 catch 子句。並且,自 Java 7之後,允許一個 catch 子句捕獲多個異常型別。

try
{
 ...
}
catch(FileNotFoundException | UnknownHostException e)//一個catch捕獲多個異常
{
 ...
}
catch(IOException e)//一個 try 語句塊中多個 catch 語句
{
 ...
}

注意:

  • 用一個catch捕獲多個異常時,異常變數隱含為final變數,因此不允許為 e 賦於不同的值。
  • 多個catch子塊存在時,由於程式是由上往下依次捕捉,不允許將父類異常寫在子類的上方。
  • catch語塊中允許再次丟擲異常(throw語句)

try-catch-finally語句:

假定這樣一種情況,一個程式在 try 中使用了一些本地資源,而且這些資源必須在程式退出時進行清理。如果只用try-catch方法,那麼每個catch語句中都要重複書寫清理資源的程式碼,顯得非常笨重且繁瑣。現在,我們可以用finally語句來解決這個問題,它的特定是:不管異常有無被捕獲,finally語句中的程式碼都會執行。功能是:確保資源被清理。

示例:在finally語句中關閉輸出流

var in = new FileInputStream(...)
try
{
 //1
 可能發生異常的語句
 //2
}
catch (IOException e)
{
 //3
 展示錯誤資訊
 //4
}
finally
{
 //5
 in.close();//關閉輸出流
}
//6

現在我們來分析一下不同情況下程式相應的執行情況:

  • try 中程式碼未丟擲異常。執行1256。
  • try 中程式碼丟擲異常並且被捕獲且 catch沒有丟擲異常。執行13456。
  • try 中程式碼丟擲異常並且被捕獲但 catch丟擲異常。執行135。
  • try 中程式碼丟擲異常但未被捕獲。執行15。

總結一下,

執行6的條件是,走完了整個 try / catch塊。

執行5的條件是,任何條件都會執行。

注意:

  • 允許一個catch塊都沒有。
  • 允許try / catch / finally子塊中巢狀一個try-catch-finally塊。
  • 千萬不要在finally子塊中使用改變控制流的語句!(return,throw,break,continue)。因為finally的執行在try之後,整個方法返回之前,因此在finally子塊中改變控制流將會覆蓋 try 中的結果。(這個結果可以是一個return值,也可以是一個異常)。

六、額外補充之try-with-Resources語句

Java中存在一個AutoCloseable介面

public interface AutoCloseable
{
  void close() throws Exception;
}

假定資源屬於一個實現了AutoCloseable介面的類,那麼處理異常時可以不需要finally子塊。因為該資源無論是正常退出或產生異常,都會自動呼叫close方法,代替了finally子塊。

帶資源的try語句通用格式:

try(Resources res = ...)
{
  work with Resources;
}

舉例如下:

try(var in = new Scanner(...))
{
  while(in.hashNext()) System.out.println(in.next());
}//無論 try塊怎麼樣退出,都會執行 in.close();

這種方式使得程式碼看起來更加簡潔。

請注意:

  • try-with-Resources語句允許有catch / finally子句。它們會在資源關閉後再執行。

最後的最後,希望提醒大家,捕獲異常雖然方便,但會極大的延長程式執行的時間,因此,只在必要條件下使用異常。

總結

到此這篇關於一篇文章解決Java異常處理的文章就介紹到這了,更多相關Java異常處理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!