如何正確使用Java異常處理機制


文章來源:leaforbook - 如何正確使用Java異常處理機制
作者:士別三日

第一節 異常處理概述

第二節 Java異常處理類

  • 2.1 Throwable
    • 2.1.1 Throwable有五種構造方法
    • 2.1.2 Throwable的所有成員方法
  • 2.2 Error
  • 2.3 Exception
  • 2.4 RuntimeException
  • 2.5 Checked Exception
  • 2.6 Uncheck Exception
  • 2.7 總結

第三節 Java異常處理執行流程探究

  • 3.1 流程一
  • 3.2 流程二
  • 3.3 流程三
  • 3.4 流程四
  • 3.5 流程五
  • 3.6 流程六
  • 3.7 流程七
  • 3.8 流程八
  • 3.9 總結

第四節 Java異常處理實踐原則

  • 4.1 使用異常,而不使用返回碼
  • 4.2 利用運行時異常設定方法使用規則
  • 4.3 消除運行時異常
  • 4.4 正確處理檢查異常
  • 4.5 使主流程代碼保持整潔
  • 4.6 使用try-with-resources
  • 4.7 盡量處理最具體的異常
  • 4.8 設計自己的異常類型要遵循的原則

本文的目標並不是介紹Java異常處理機制相關概念和語法,如果你有這方面的需求,請參考我的“ Java異常 官方文檔翻譯系列”文章。本文的目標是如何正確使用Java異常處理機制。

第一節 異常處理概述

在理想境界中,程序永遠不會出現問題,用戶輸入的數據永遠是正確的,邏輯沒有任何問題 ,選擇打開的文件也一定是存在的,內存永遠是夠用的……反正沒有任何問題!但是一旦出現這些問題,如果處理不好,程序就不能正常運行了,用戶就有可能再也不使用這個程序了。

要處理異常,必先知道異常發生的原因;要知道異常發生的原因,必先知道異常發生的場景。你的程序可能和任何其他實體交互的時候,都可能發生異常。Java程序中,都是利用方法(Method)和其他實體進行交互。所以異常的發生、拋出、聲明和處理都是在方法內。下圖是Java程序可能和其他實體交互的概要圖:

圖1:你的方法與其他實體交互概要圖

如圖1所示,你寫的方法和外部實體交互大概可以分為五類:

  1. 和資源(Resource)交互,見圖⑤處。這里資源的范圍很廣,比如進程外部的數據庫,文件,SOA服務,其他各種中間件;進程內的類,方法,線程……都算是資源。
  2. 給進程內的其他方法(User Method)提供服務,見圖②處。
  3. 依賴進程內的其他方法(Server Method),見圖③處。包括Java平台提供的方法和其他第三方供應方提供的方法。
  4. 和系統環境交互,見圖⑧處。系統環境可能是直接環境——JVM,也可能是間接環境——操作系統或硬件等。
  5. 給外部實體提供服務,見圖①處。這種外部實體一般會通過容器(或其他類似的機制)和你的方法進行交互。所以,可以歸為②,不予探討。

Java方法和每一類實體進行交互時,都可能發生異常。

當和資源交互時,常常會因為資源不可用而發生異常,比如發生找不到文件、數據庫連接錯誤、找不到類、找不到方法……等等狀況。有可能是直接產生的,見圖⑤處;有可能是間接產生的,比如圖⑥處發生異常,Server Method把異常拋給Your Method,圖③處就間接發生了異常。一般來說,你寫的方法間接發生這類異常的可能性比直接發生要大得多,因為直接產生這類異常的方法在Java平台中已經提供了。對於這類異常,通常有以下幾個特點:

  • 問題來自外部,你的方法本身的正常流程邏輯沒問題。
  • 這類異常通常是暫時的,過段時間就可用了(經過資源維護者的徹夜奮戰……)。最終用戶通常也可以接受暫時的等待或采取替補方案。
  • 你的程序的其他功能還可以用。

這時,你的方法應該這樣處理:

  • 返回到一種安全狀態,並能夠讓用戶執行一些其他的命令(比如支付寶支付失敗時返回一個彈出框,說明余額不足等原因,讓用戶重新選擇其他支付渠道);或者
  • 允許用戶保存所有操作的結果,並以適當的方式終止程序(比如保存填了一半的表單)。

然后,你應該協調各方,促進資源恢復可用,消除異常。

當給用戶方法(User Method )提供服務時,用戶可能會傳入一些不合法的數據(或者其他不恰當的使用方法),進而對程序的正常流程造成破壞。你的方法應該檢查每一個輸入數據,如果發現不合法的數據,馬上阻止執行流程,並通知用戶方法。

當調用服務方法(Server Method )時,有可能會發生兩類異常。一類是你的使用方法不正確,導致服務中止;一類是服務方法出了異常,然后傳遞給你的方法。如果是第一種異常,你應該檢查並修改你的方法邏輯,消除BUG。對於第二類異常,你要么寫一個處理器處理,要么繼續傳遞給上層方法。

當和系統環境交互時,有可能因為JVM參數設置不當,有可能因為程序產生了大量不必要的對象,也有可能因為硬故障(操作系統或硬件出了問題),導致整個程序不可用。當這類異常發生時,最終用戶沒法選擇其他替代方案,操作到一半的數據會全部丟失。你的方法對這類異常一般沒什么辦法,既不能通過修改主流程邏輯來消除,也不能通過增加異常處理器來處理。所以通常你的方法對這類異常不需要做任何處理。但是你必須檢查進程內的所有程序和系統環境是否正常,然后協調各方,修改BUG或恢復環境。

Java的異常都是發生在方法內,所以研究Java異常,要以你設計的方法為中心。我們以“你的方法 ”為中心,總結一下處理辦法:當服務方法告訴“你的方法 ”的主流程邏輯有問題時,就要及時修復BUG來消除異常;當用戶方法非法使用“你的方法”時,應該直接中止主流程,並通知用戶方法,強迫用戶方法使用正確的方式,防止問題蔓延;當服務方法傳遞一個異常給“你的方法”時,你要判斷“你的方法”是否合適處理這個異常,如果不合適,傳遞給上層方法,如果合適,寫一個異常處理器處理這個異常。當系統環境出了問題,“你的方法”什么也做不了。

剛才以“你的方法”為中心,總結了在“你的方法”內部的處理辦法。現在以“你”為中心,總結一下方法外部的處理方法:當資源不可用的時候,你應該協調各方,恢復資源;當發生系統故障時,你應該協調各方,恢復系統。

現在,已經基本分析清楚了異常發生的原因,以及相應的應對方法。下一節正式介紹Java異常處理機制。

第二節 Java異常處理類

Java把異常當做是破壞正常流程的一個事件,當事件發生后,就會觸發處理機制。

Java有一套獨立的異常處理機制,在遇到異常時,方法並不返回任何值(返回值屬於正常流程),而是拋出一個封裝了錯誤信息的對象。下圖是Java異常處理機制類層級結構圖:

圖2:Java異常處理機制類層級結構圖

2.1 Throwable

所有的異常對象都派生於Throwable類的一個實例。

2.1.1 Throwable有五種構造方法:

備注:

  • suppression:被壓抑的異常。想了解更多信息,請參看我的譯文“try-with-resources語句”。
  • strack trace:堆棧跟蹤。是一個方法調用過程列表,它包含了程序執行過程中方法調用的具體位置。

2.1.2 Throwable的所有成員方法:

備注:

  • 所有派生於Throwable類的異常類,基本都沒有這些成員方法,也就是說所有的異常類都只是一個標記,記錄發生了什么類型的異常(通過標記,編譯期和JVM做不同的處理),所有實質性的行為Throwable都具備了。
  • 綜上,在一個Throwable里面可以獲取什么信息?
    • 獲取堆棧跟蹤信息(源代碼中哪個類,哪個方法,第幾行出現了問題……從當前代碼到最底層的代碼調用鏈都可以查出來)
    • 獲取引發當前Throwable的Throwable。追蹤獲取底層的異常信息。
    • 獲取被壓抑了,沒拋出來的其他Throwable。一次只能拋出一個異常,如果發生了多個異常,其他異常就不會被拋出,這時可以通過加入suppressed異常列表來解決(JDK7以后才有)。
    • 獲取基本的詳細描述信息

從圖2可以看出,Throwable類只有兩個直接繼承者:Error和Exception。然后Exception又分為RuntimeException和Checked Exception。

2.2 Error

在Java中, 由系統環境問題引起的異常,一般都繼承於Error類。

對於Error類:

  • 一般開發者不要自定義Error子類,因為它代表系統級別的錯誤。與一般的程序無關。
  • 在Java異常處理機制中,Error不強制捕獲或聲明,也就是不強制處理。因為程序本身對此類錯誤無能為力。一般情況下我們只要把堆棧跟蹤信息記錄下來就行。

下列是Java平台中直接繼承於Error的錯誤類型:

AnnotationFormatErrorAssertionErrorAWTErrorCoderMalfunctionErrorFactoryConfigurationError,FactoryConfigurationErrorIOErrorLinkageErrorServiceConfigurationErrorThreadDeath,TransformerFactoryConfigurationErrorVirtualMachineError

2.3 Exception

在Java中,除了系統環境問題引起的異常,一般都繼承於Exception類。Exception分為RuntimeException和Checked Exception。Checked Exception必須要捕獲或聲明。而RuntimeException不強制。

對於Exception類:

  • 如果你創建了一個異常類型,直接繼承於Exception,那么這個異常類型將屬於檢查異常(Checked Exception)。

2.4 RuntimeException

在Java中,由於接口方法使用不當造成的異常,一般屬於RuntimeException,也就是運行時異常。

對於RuntimeException:

  • 如果你調用服務方法的方式不正確,你應該馬上修改代碼,避免發生RuntimeException
  • 如果是用戶方法調用你的方法的方式不正確,你應該立刻拋出RuntimeException,強制讓使用者修正代碼或改變使用方式,防止問題蔓延
  • 一般情況下,不要捕獲或聲明RuntimeException。因為問題在於你的程序本身有問題,如果你用異常流程處理了,反而讓正常流程問題一直存在

下列是Java平台中直接繼承於RuntimeException的運行時異常:

AnnotationTypeMismatchExceptionArithmeticExceptionArrayStoreExceptionBufferOverflowException,BufferUnderflowExceptionCannotRedoExceptionCannotUndoExceptionClassCastException,CMMExceptionConcurrentModificationException,DataBindingExceptionDOMException,EmptyStackExceptionEnumConstantNotPresentExceptionEventException,FileSystemAlreadyExistsExceptionFileSystemNotFoundExceptionIllegalArgumentException,IllegalMonitorStateException,IllegalPathStateExceptionIllegalStateExceptionIllformedLocaleException,ImagingOpException,IncompleteAnnotationExceptionIndexOutOfBoundsException,JMRuntimeExceptionLSExceptionMalformedParameterizedTypeException,MirroredTypesException,MissingResourceExceptionNegativeArraySizeException,NoSuchElementExceptionNoSuchMechanismExceptionNullPointerExceptionProfileDataException,ProviderExceptionProviderNotFoundExceptionRasterFormatExceptionRejectedExecutionException,SecurityExceptionSystemExceptionTypeConstraintExceptionTypeNotPresentException,UndeclaredThrowableExceptionUnknownEntityExceptionUnmodifiableSetException,UnsupportedOperationExceptionWebServiceExceptionWrongMethodTypeException

2.5 Checked Exception

在Java中,直接或間接因為“資源”問題引起的異常,一般屬於檢查異常(Checked Exception) 。檢查異常繼承於Exception,而不繼承於RuntimeException。

對於檢查異常:

  • 必須捕獲或聲明
  • 交給關心這個異常的方法處理
  • 異常處理器應該引導用戶接下來怎么辦,至少做到安全退出

下列是Java平台中直接繼承於Exception的檢查異常:

AclNotFoundExceptionActivationExceptionAlreadyBoundExceptionApplicationExceptionAWTException,BackingStoreExceptionBadAttributeValueExpExceptionBadBinaryOpValueExpException,BadLocationExceptionBadStringOperationException,BrokenBarrierExceptionCertificateException,CloneNotSupportedExceptionDataFormatExceptionDatatypeConfigurationExceptionDestroyFailedException,ExecutionExceptionExpandVetoExceptionFontFormatExceptionGeneralSecurityException,GSSException,IllegalClassFormatExceptionInterruptedExceptionIntrospectionExceptionInvalidApplicationException,InvalidMidiDataExceptionInvalidPreferencesFormatExceptionInvalidTargetObjectTypeException,IOExceptionJAXBExceptionJMException,KeySelectorExceptionLastOwnerException,LineUnavailableExceptionMarshalExceptionMidiUnavailableExceptionMimeTypeParseException,MimeTypeParseExceptionNamingExceptionNoninvertibleTransformExceptionNotBoundException,NotOwnerExceptionParseExceptionParserConfigurationExceptionPrinterExceptionPrintException,PrivilegedActionExceptionPropertyVetoExceptionReflectiveOperationExceptionRefreshFailedException,RemarshalExceptionSAXException,ScriptExceptionServerNotActiveExceptionSOAPException,SQLExceptionTimeoutExceptionTooManyListenersExceptionTransformerExceptionTransformException,UnmodifiableClassExceptionUnsupportedAudioFileExceptionUnsupportedCallbackException,UnsupportedFlavorExceptionUnsupportedLookAndFeelExceptionURIReferenceExceptionURISyntaxExceptionUserExceptionXAExceptionXMLParseExceptionXMLSignatureException,XMLStreamExceptionXPathException

2.6 Uncheck Exception

Error和RuntimeException統稱為非檢查異常。兩者的共同點就是都不被強制捕獲或聲明。實際上兩者描述問題的范圍完全沒有交集。

2.7 總結

所有的功能都在Throwable類里面實現了,子類只需要直接繼承或間接繼承它,並且加上需要的構造方法就行(一般而言,第一第二個構造方法是必須的,也可以全部加上),而且構造方法通常只需要一行代碼:super(...),也就是說只要調用父類的構造方法就行了。Java把異常分為三類(Error,Checked Exception,RuntimeException),只是在語法層面上有不同的標記而已。它們自身擁有的功能一樣,運行時系統處理它們的方式也是一樣的(你也可以捕獲或聲明非檢查異常),不同的是編譯器對它們的區別對待(檢查異常必須要在代碼里處理,非檢查異常就不需要),以及程序員對它們的區別對待(這需要程序員遵循良好的實踐原則)。

這三類異常全部覆蓋了第一節中所描述的異常發生場景,圖1中,④⑤⑥處可能會發生Checked Exception,②③處既可能會發生RuntimeException也可能會發生Checked Exception,⑦⑧⑨處可能會發生Error。①處已經超出了Java異常處理機制的范疇(這屬於容器要考慮的問題),通常在數據中加入返回碼來通知異常信息。

理解了每一類異常對應的場景,很多人其實已經知道該怎么用了,不必往下看了。

第三節 Java異常處理執行流程探究

首先設計兩個方法,一個方法可能會拋出RuntimeException,一個方法可能會拋出Checked Exception。

public String runtimeServerMethod(String s) {
  if(s==null) {
     throw new RuntimeException("runtimeServerMethod方法的字符串不能為空")
  }
  return s;
}
private BufferedReader bufferedReader;
public String checkedServerMethod(String s) throws IOException {
   File file = new File(s);
   Reader reader = new FileReader(file);
   bufferedReader = new BufferedReader(reader);
   String result = bufferedReader.readLine();
   return result;
}

3.1 流程一

public void currentMethod() {
  System.out.println("--------------------try-catch-before");
  String result = this.runtimeServerMethod(null);
  System.out.println("--------------------result:"+result);
  System.out.println("--------------------try-catch-after");
}

執行結果:

--------------------try-catch-before
Exception in thread "main" java.lang.RuntimeException: runtimeServerMethod方法的字符串不能為空
     at com.leaforbook.javaexception.App.runtimeServerMethod(App.java:34)
     at com.leaforbook.javaexception.App.currentMethod(App.java:26)
     at com.leaforbook.javaexception.App.userMethod(App.java:20)
     at com.leaforbook.javaexception.App.main(App.java:16)

分析:

違反了runtimeServerMethod方法的使用規則——入參不能為null,導致產生了一個運行時異常。主流程線程直接中斷,后面的代碼不再執行。

3.2 流程二

public void currentMethod() {
  System.out.println("--------------------try-catch-before");
  String result = null;
  try {
     result = this.runtimeServerMethod(null);
     System.out.println("--------------------result:"+result);
  } catch (Exception e) {
     System.out.println("--------------------in-catch");
  }
  System.out.println("--------------------try-catch-after");
}

執行結果:

--------------------try-catch-before
--------------------in-catch
--------------------try-catch-after

分析:

RuntimeException也可以捕獲處理(這是一種不好的實踐),運行時系統並不會區分異常類型。異常發生以后,try代碼塊后面的代碼不再執行,而是跳到catch代碼塊,線程不中斷,執行完整個方法。

3.3 流程三

public void currentMethod() {
   System.out.println("--------------------try-catch-before");
   String result = null;
   result = this.runtimeServerMethod("Conform to the rules");
   System.out.println("--------------------result:"+result);
   System.out.println("--------------------try-catch-after");
}

執行結果:

--------------------try-catch-before
--------------------result:Conform to the rules
--------------------try-catch-after

分析:

當符合服務方法的規則時,就不會拋出運行時異常。方法就可以正常執行完成。

3.4 流程四

public void currentMethod() {
     System.out.println("--------------------try-catch-before");
     String result = null;
     try {
              result = this.checkedServerMethod("");
               System.out.println("--------------------result:"+result);
          } catch (IOException e) {
              System.out.println("--------------------in-catch");
          } finally {
               System.out.println("--------------------in-finally");
              if(bufferedReader!=null) {
                   try {
                        bufferedReader.close();
                   } catch (IOException e) {
                        e.printStackTrace();
                   }
              }
          }
     System.out.println("--------------------try-catch-after");
}

執行結果:

--------------------try-catch-before
--------------------in-catch
--------------------in-finally
--------------------try-catch-after

分析:

當調用了checkedServerMethod方法,並且發生了Checked Exception時,一定要捕獲或聲明該異常,否則編譯不通過。上例中,異常發生后,try代碼塊后面的代碼不再執行,跳到catch代碼塊,再執行finally代碼塊(在這里有關閉資源的操作),然后再執行其余部分。

3.5 流程五

public void currentMethod() {
     System.out.println("--------------------try-catch-before");
     String result = null;
     try {
              result = this.checkedServerMethod("");
               System.out.println("--------------------result:"+result);
          } catch (IOException e) {
              System.out.println("--------------------in-catch");
              return;
          } finally {
               System.out.println("--------------------in-finally");
              if(bufferedReader!=null) {
                   try {
                        bufferedReader.close();
                   } catch (IOException e) {
                        e.printStackTrace();
                   }
              }
          }
     System.out.println("--------------------try-catch-after");
 }

執行結果:

--------------------try-catch-before
--------------------in-catch
--------------------in-finally

分析:

和流程四不同之處在於,catch代碼塊多了一條return語句。執行結果也相應發生了變化,try-catch-finally代碼塊后面的代碼不再執行。而且值得注意的是,finally代碼塊的代碼依然執行了。這就是finally代碼塊的意義。

3.6 流程六

public void userMethod() {
    try {
        this.currentMethod();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
public void currentMethod() throws IOException {
     System.out.println("--------------------try-catch-before");
     String result = null;
     try {
              result = this.checkedServerMethod("");
               System.out.println("--------------------result:"+result);
          } catch (IOException e) {
              System.out.println("--------------------in-catch");
              this.checkedServerMethod("catch");
          } finally {
               System.out.println("--------------------in-finally");
              if(bufferedReader!=null) {
                   try {
                        bufferedReader.close();
                   } catch (IOException e) {
                        e.printStackTrace();
                   }
              }
          }
         System.out.println("--------------------try-catch-after");
}

執行結果:

--------------------try-catch-before
--------------------in-catch
--------------------in-finally
java.io.FileNotFoundException: catch (系統找不到指定的文件。)
     at java.io.FileInputStream.open0(Native Method)
     at java.io.FileInputStream.open(FileInputStream.java:195)
     at java.io.FileInputStream.<init>(FileInputStream.java:138)
     at java.io.FileReader.<init>(FileReader.java:72)
     at com.leaforbook.javaexception.App.checkedServerMethod(App.java:62)
     at com.leaforbook.javaexception.App.currentMethod(App.java:38)
     at com.leaforbook.javaexception.App.userMethod(App.java:23)
     at com.leaforbook.javaexception.App.main(App.java:18)

分析:

和流程五類似,只不過catch代碼塊不是返回一個正常值,而是拋出一個Checked Exception。

3.7 流程七

public void userMethod() {
  try {
      this.currentMethod();
  } catch (IOException e) {
      e.printStackTrace();
  }
}
public void currentMethod() throws IOException {
     System.out.println("--------------------try-catch-before");
     String result = null;
     try {
              result = this.checkedServerMethod("");
               System.out.println("--------------------result:"+result);
          } catch (IOException e) {
              System.out.println("--------------------in-catch");
          this.checkedServerMethod("catch");
          } finally {
               System.out.println("--------------------in-finally");
              if(bufferedReader!=null) {
                   try {
                        bufferedReader.close();
                   } catch (IOException e) {
                        e.printStackTrace();
                   }
              }
              this.checkedServerMethod("finally");
          }
      System.out.println("--------------------try-catch-after");
}

執行結果:

--------------------try-catch-before
--------------------in-catch
--------------------in-finally
java.io.FileNotFoundException: finally (系統找不到指定的文件。)
     at java.io.FileInputStream.open0(Native Method)
     at java.io.FileInputStream.open(FileInputStream.java:195)
     at java.io.FileInputStream.<init>(FileInputStream.java:138)
     at java.io.FileReader.<init>(FileReader.java:72)
     at com.leaforbook.javaexception.App.checkedServerMethod(App.java:63)
     at com.leaforbook.javaexception.App.currentMethod(App.java:48)
     at com.leaforbook.javaexception.App.userMethod(App.java:23)
     at com.leaforbook.javaexception.App.main(App.java:18)

分析:

和流程六比起來,流程七在finally代碼塊也拋出了一個異常,最終在userMethod方法里面捕獲到的是finally代碼塊的異常,catch代碼塊里拋出的異常被壓抑了。

3.8 流程八

public void userMethod() {
     try {
         this.currentMethod();
     } catch (IOException e) {
         e.printStackTrace();
     }
}
public void currentMethod() throws IOException {
     System.out.println("--------------------try-catch-before");
     String result = null;
     try {
              result = this.checkedServerMethod("");
               System.out.println("--------------------result:"+result);
          }finally {
               System.out.println("--------------------in-finally");
              if(bufferedReader!=null) {
                   try {
                        bufferedReader.close();
                   } catch (IOException e) {
                        e.printStackTrace();
                   }
              }
          }
     System.out.println("--------------------try-catch-after");
 }

執行結果:

--------------------try-catch-before
--------------------in-finally
java.io.FileNotFoundException:
     at java.io.FileInputStream.open0(Native Method)
     at java.io.FileInputStream.open(FileInputStream.java:195)
     at java.io.FileInputStream.<init>(FileInputStream.java:138)
     at java.io.FileReader.<init>(FileReader.java:72)
     at com.leaforbook.javaexception.App.checkedServerMethod(App.java:59)
     at com.leaforbook.javaexception.App.currentMethod(App.java:34)
     at com.leaforbook.javaexception.App.userMethod(App.java:23)
     at com.leaforbook.javaexception.App.main(App.java:18)

分析:

如果沒有catch代碼塊,強制要求聲明拋出異常。userMethod捕獲到的是try代碼塊拋出的異常(如果finally代碼塊也拋出異常,這個異常就會被壓抑)。finally代碼塊還是會執行。

3.9 總結

  • 在運行時環境,並不會區分異常的類型,所以程序員自己要遵從良好的實踐原則,否則Java異常處理機制就會被誤用。
  • 然后finally代碼塊總是會在方法返回或方法拋出異常前執行,而try-catch-finally代碼塊后面的代碼就有可能不會再執行。
  • finally代碼塊里面不推薦使用return語句或throw語句。
  • try代碼塊一定要求要有一個catch代碼塊或finally代碼塊(二者取其一就行)。
  • catch處理器的優先級比聲明異常語句要高。
  • 如果多處拋出異常,finally代碼塊里面的異常會壓抑其他異常。

第四節 Java異常處理實踐原則

4.1 使用異常,而不使用返回碼

關於這一點,在我的譯文“使用異常的優勢”有很詳細的描述。理解了這一點,程序員們才會想要使用Java異常處理機制。

4.2 利用運行時異常設定方法使用規則

很常見的例子就是,某個方法的參數不能為空。在實踐中,很多程序員的處理方式是,當傳入的這個參數為空的時候,就返回一個特殊值(最常見的就是返回一個null,讓用戶方法決定怎么辦)。還有的處理方式是,自己給一個默認值去兼容這種不合法參數,自己決定怎么辦。這兩種實踐都是不好的。

對於第一種處理方式,返回值是用來處理正常流程的,如果用來處理異常流程,就會讓用戶方法的正常流程變復雜。一次調用可能不明顯,當有多個連續調用就會變得很復雜了。對於第二種處理方式,看起來很強大,因為“容錯”能力看起來很強,有些程序員甚至可能會為此沾沾自喜。但是它也一樣讓正常流程變復雜了,這不是最糟糕的,最糟糕的是,你不知道下一次用戶會出什么鬼點子,傳個你現有處理代碼處理不了的東西進來。這樣你又得加代碼,繼續變復雜……BUG就是這樣產生的。

好的實踐方式就是,設定方法的使用規則,遇到不合法的使用方式時,立刻拋出一個運行時異常。這樣既不會讓主流程代碼變復雜,也不會制造不必要的BUG。為什么是運行時異常而不是檢查異常呢?這是為了強迫用戶修改代碼或者改正使用方式——這屬於用戶的使用錯誤。

4.3 消除運行時異常

當你的程序發生運行時異常,通常都是因為你使用別人的方法的方式不正確(如果設計這個異常的人設計錯誤,就另當別論。比如設計者捕獲一個檢查異常,然后在處理器拋出一個運行時異常給用戶。如果遇上這樣的供應商,還是棄用吧)。所以,一般都是采取修改代碼的方式,而不是新增一個異常流程。

4.4 正確處理檢查異常

處理檢查異常的時候,處理器一定要做到下面的要求才算合格:

  • 返回到一種安全狀態,並能夠讓用戶執行一些其他的命令;或者
  • 允許用戶保存所有操作的結果,並以適當的方式終止程序。

不好的實踐案例一:因為有的異常發生的概率很小,有些程序員就會寫出下面的代碼:

public Image loadImage(String s) {
     try {
          code...
     } catch (Exception e)
     {}
     code2...
}

catch代碼塊里面什么都不寫!或者只在里面打一個log。這樣既不會傳遞到上層方法,又不會報編譯錯誤,還不用動腦筋……

不好的實踐案例二:捕獲一個檢查異常,什么都不做(或只打一個log),然后拋出一個運行時異常:

public Image loadImage(String s) {
     try {
          code...
     } catch (Exception e){
          throw new RuntimeExcepiton();
     }
}

這樣也不會讓上層方法感覺到這個異常的存在,也不會報編譯錯誤了,也不用動什么腦筋……

在案例一中,一旦出現了異常,try代碼塊里的代碼沒執行完,用戶要求做的事情沒做完,卻又沒有任何反饋或者得到一個錯誤反饋。

在案例二中,一旦出現了異常,try代碼塊里的代碼沒執行完,雖然把運行時異常拋給用戶了,用戶也不會去處理這個異常,又沒有辦法通過改變使用方式消除異常,直接讓用戶代碼崩潰掉。

對於檢查異常,好的實踐方式是:

  • 讓可以處理這個異常的方法去處理。衡量的標准就是在你這個方法寫一個處理器,這個處理器能不能做到本節開頭的那兩個要求,如果不能,就往上拋。如果你不能知道所有用戶的所有需求,你通常就做不到那兩個要求。
  • 有必要的時候可以通過鏈式異常包裝一下,再拋出。
  • 最終的處理器一定要做到本節開頭的那兩個要求。

4.5 使主流程代碼保持整潔

一個try代碼塊后面可以跟多個catch代碼塊,這就讓一些可能會發生不同異常的代碼可以寫在一塊,讓代碼看起來很清晰。相反,在一個方法里寫多個try-catch,或者寫嵌套的try-catch,就會讓主流程代碼變得很混亂。

4.6 使用try-with-resources

請參看我的譯文“try-with-resources語句”。

try-with-resources語句比起普通的try語句,干凈整潔的多。而且最終拋出的異常是正常流程中拋出的異常。

4.7 盡量處理最具體的異常

盡量使用最具體的異常類作為處理器匹配的類型。這樣處理器就不用兼顧很多種情形,不易出錯。從Java7開始,一個處理器可以處理多種異常類型。

注意:同一個try語句中,比較具體的異常的catch代碼塊應寫在前面,比較通用的異常的catch代碼塊應寫在后面。

4.8 設計自己的異常類型要遵循的原則

當你是一個模塊開發者,你就很有必要設計一組或多組自己的異常類型。一般情況下,要遵守如下原則:

  • 確定什么場景下,需要創建自己的異常類型:(參看我的譯文“創建異常類”)。
  • 為你的接口方法的使用規則創建一組運行時異常。
  • 包裝別人的檢查異常的時候,一定也要用檢查異常。這樣異常才能傳遞給上層方法處理。
  • 設計一組有層次結構的異常,而不是設計一堆零零散散的異常。
  • 區分清楚異常發生的原因,然后決定你的異常是檢查異常還是運行時異常。
  • 模塊內部不需要處理自己定義的異常。

Java異常處理機制的目的至少有三個:一是歸類處理不同的異常,二是提供足夠的信息方便調試,三是讓主流程代碼保持整潔。

——————————————————————————————————————————————————————————————————————————————————

在學習過程如果有任何疑問,請來極樂網(www.)提問,或者掃描下方二維碼,關注極樂官方微信,在平台下方留言~

轉自:https://zhuanlan.zhihu.com/p/23371163


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM