有效處理 Java 異常三原則

2022-01-24 嵌入式資訊精選

Java中異常提供了一種識別及響應錯誤情況的一致性機制,有效地異常處理能使程序更加健壯、易於調試。

異常之所以是一種強大的調試手段,在於其回答了以下三個問題:

在有效使用異常的情況下,異常類型回答了「什麼」被拋出,異常堆棧跟蹤回答了「在哪「拋出,異常信息回答了「為什麼「會拋出,如果你的異常沒有回答以上全部問題,那麼可能你沒有很好地使用它們。

有三個原則可以幫助你在調試過程中最大限度地使用好異常,這三個原則是:

為了闡述有效異常處理的這三個原則,本文通過杜撰個人財務管理器類JCheckbook進行討論,JCheckbook用於記錄及追蹤諸如存取款,票據開具之類的銀行帳戶活動。

具體明確

Java定義了一個異常類的層次結構,其以Throwable開始,擴展出Error和Exception,而Exception又擴展出RuntimeException.如圖1所示.

圖1.Java異常層次結構

這四個類是泛化的,並不提供多少出錯信息,雖然實例化這幾個類是語法上合法的(如:new Throwable()),但是最好還是把它們當虛基類看,使用它們更加特化的子類。Java已經提供了大量異常子類,如需更加具體,你也可以定義自己的異常類。

例 如:java.io package包中定義了Exception類的子類IOException,更加特化確的是 FileNotFoundException,EOFException和ObjectStreamException這些IOException的子 類。

每一種都描述了一類特定的I/O錯誤:分別是文件丟失,異常文件結尾和錯誤的序列化對象流。異常越具體,我們的程序就能更好地回答」什麼出了錯」這個問題

捕獲異常時儘量明確也很重要。

例如:JCheckbook可以通過重新詢問用戶文件名來處理FileNotFoundException,對於 EOFException,它可以根據異常拋出前讀取的信息繼續運行。

如果拋出的是ObjectStreamException,則程序應該提示用戶文件 已損壞,應當使用備份文件或者其他文件。

Java讓明確捕獲異常變得容易,因為我們可以對同一try塊定義多個catch塊,從而對每種異常分別進行恰當的處理。

File prefsFile = new File(prefsFilename);

 

try{

    readPreferences(prefsFile);

}

catch (FileNotFoundException e){

    // alert the user that the specified file

    // does not exist

}

catch (EOFException e){

    // alert the user that the end of the file

    // was reached

}

catch (ObjectStreamException e){

     // alert the user that the file is corrupted

}

catch (IOException e){

    // alert the user that some other I/O

    // error occurred

}

JCheckbook 通過使用多個catch塊來給用戶提供捕獲到異常的明確信息。

舉例來說:如果捕獲了FileNotFoundException,它可以提示用戶指定另一 個文件,某些情況下多個catch塊帶來的額外編碼工作量可能是非必要的負擔,但在這個例子中,額外的代碼的確幫助程序提供了對用戶更友好的響應。

除前三個catch塊處理的異常之外,最後一個catch塊在IOException拋出時給用戶提供了更泛化的錯誤信息.這樣一來,程序就可以儘可能提供具體的信息,但也有能力處理未預料到的其他異常。

有時開發人員會捕獲範化異常,並顯示異常類名稱或者列印堆棧信息以求"具體"。

千萬別這麼幹!

用戶看到java.io.EOFException或者堆棧信息 只會頭疼而不是獲得幫助。應當捕獲具體的異常並且用"人話"給用戶提示確切的信息。不過,異常堆棧倒是可以在你的日誌文件裡列印。記住,異常和堆棧信息是用來幫助開發人 員而不是用戶的。

最後,應該注意到JCheckbook並沒有在readPreferences()中捕獲異常,而是將捕獲和處理異常留到用戶界面層來做,這樣就能用對話框或其他方式來通知用戶。這被稱為"延遲捕獲",下文就會談到。

提早拋出

異常堆棧信息提供了導致異常出現的方法調用鏈的精確順序,包括每個方法調用的類名,方法名,代碼文件名甚至行數,以此來精確定位異常出現的現場。

java.lang.NullPointerException

at java.io.FileInputStream.open(Native Method)

at java.io.FileInputStream.<init>(FileInputStream.java:103)

at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225)

at jcheckbook.JCheckbook.startup(JCheckbook.java:116)

at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)

at jcheckbook.JCheckbook.main(JCheckbook.java:318)

以上展示了FileInputStream類的open()方法拋出NullPointerException的情況。

不過注意 FileInputStream.close()是標準Java類庫的一部分,很可能導致這個異常的問題原因在於我們的代碼本身而不是Java API。所以問題很可能出現在前面的其中一個方法,幸好它也在堆棧信息中列印出來了。

不幸的是,NullPointerException是Java中信息量最少的(卻也是最常遭遇且讓人崩潰的)異常。它壓根不提我們最關心的事情:到底哪裡是null。所以我們不得不回退幾步去找哪裡出了錯。

通過逐步回退跟蹤堆棧信息並檢查代碼,我們可以確定錯誤原因是向readPreferences()傳入了一個空文件名參數。既然readPreferences()知道它不能處理空文件名,所以馬上檢查該條件:

public void readPreferences(String filename)

throws IllegalArgumentException{

    if (filename == null){

         throw new IllegalArgumentException("filename is null");

    }  //if

 

   //...perform other operations...

 

   InputStream in = new FileInputStream(filename);

 

   //...read the preferences file...

}

通過提早拋出異常(又稱"迅速失敗"),異常得以清晰又準確。

堆棧信息立即反映出什麼出了錯(提供了非法參數值),為什麼出錯(文件名不能為空值),以及哪裡出的錯(readPreferences()的前部分)。這樣我們的堆棧信息就能如實提供:

java.lang.IllegalArgumentException: filename is null

at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207)

at jcheckbook.JCheckbook.startup(JCheckbook.java:116)

at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)

at jcheckbook.JCheckbook.main(JCheckbook.java:318)

另外,其中包含的異常信息("文件名為空")通過明確回答什麼為空這一問題使得異常提供的信息更加豐富,而這一答案是我們之前代碼中拋出的NullPointerException所無法提供的。

通過在檢測到錯誤時立刻拋出異常來實現迅速失敗,可以有效避免不必要的對象構造或資源佔用,比如文件或網絡連接。同樣,打開這些資源所帶來的清理操作也可以省卻。

延遲捕獲

菜鳥和高手都可能犯的一個錯是,在程序有能力處理異常之前就捕獲它。Java編譯器通過要求檢查出的異常必須被捕獲或拋出而間接助長了這種行為。自然而然的做法就是立即將代碼用try塊包裝起來,並使用catch捕獲異常,以免編譯器報錯。

問題在於,捕獲之後該拿異常怎麼辦?最不該做的就是什麼都不做。

空的catch塊等於把整個異常丟進黑洞,能夠說明何時何處為何出錯的所有信息都會永遠丟失。把異常寫到日誌中還稍微好點,至少還有記錄可查。

但我們總不能指望用戶去閱讀或者理解日誌文件和異常信息。

讓readPreferences()顯示錯誤信息對話框也不合適,因為雖然JCheckbook目前是桌面應用程式,但我們還計劃將它變成基於HTML的Web應用。那樣的話,顯示錯誤對話框顯然不是個選擇。

同時,不管HTML還是C/S版本,配置信息都是在伺服器上讀取的,而錯誤信息需要顯示給Web瀏覽器或者客戶端程序。 readPreferences()應當在設計時將這些未來需求也考慮在內。適當分離用戶界面代碼和程序邏輯可以提高我們代碼的可重用性。

在有條件處理異常之前過早捕獲它,通常會導致更嚴重的錯誤和其他異常。

例如,如果上文的readPreferences()方法在調用FileInputStream構造方法時立即捕獲和記錄可能拋出的FileNotFoundException,代碼會變成下面這樣:

public void readPreferences(String filename){

   //...

   InputStream in = null;

   // DO NOT DO THIS!!!

try{

    in = new FileInputStream(filename);

}

catch (FileNotFoundException e){

    logger.log(e);

}

in.read(...);

//...

}

上面的代碼在完全沒有能力從FileNotFoundException中恢復過來的情況下就捕獲了它。如果文件無法找到,下面的方法顯然無法讀取它。

如果 readPreferences()被要求讀取不存在的文件時會發生什麼情況?當然,FileNotFoundException會被記錄下來,如果我們當時去看日誌文件的話,就會知道。

然而當程序嘗試從文件中讀取數據時會發生什麼?既然文件不存在,變量in就是空的,一個 NullPointerException就會被拋出。

調試程序時,本能告訴我們要看日誌最後面的信息。那將會是NullPointerException,非常讓人討厭的是這個異常非常不具體。錯誤信息不僅誤導我們什麼出了錯(真正的錯誤是FileNotFoundException而不是NullPointerException),還誤導了錯誤的出處。

真正的問題出在拋出NullPointerException處的數行之外,這之間有可能存在好幾次方法的調用和類的銷毀。我們的注意力被這條小魚從真正的錯誤處吸引了過來,一直到我們往回看日誌才能發現問題的源頭。

既然readPreferences() 真正應該做的事情不是捕獲這些異常,那應該是什麼?看起來有點有悖常理,通常最合適的做法其實是什麼都不做,不要馬上捕獲異常

把責任交給 readPreferences()的調用者,讓它來研究處理配置文件缺失的恰當方法,它有可能會提示用戶指定其他文件,或者使用默認值,實在不行的話也許警告用戶並退出程序。

把異常處理的責任往調用鏈的上遊傳遞的辦法,就是在方法的throws子句聲明異常。在聲明可能拋出的異常時,注意越具體越好。這用於標識出調用你方法的程序需要知曉並且準備處理的異常類型。例如,「延遲捕獲」版本的readPreferences()可能是這樣的:

public void readPreferences(String filename)

throws IllegalArgumentException,

FileNotFoundException, IOException{

    if (filename == null){

           throw new IllegalArgumentException("filename is null");

     }  //if

 

     //...

 

     InputStream in = new FileInputStream(filename);

 

//...

}

技術上來說,我們唯一需要聲明的異常是IOException,但我們明確聲明了方法可能拋出FileNotFoundException。 IllegalArgumentException不是必須聲明的,因為它是非檢查性異常(即RuntimeException的子類)。然而聲明它是為 了文檔化我們的代碼(這些異常也應該在方法的JavaDocs中標註出來)。

當然,最終你的程序需要捕獲異常,否則會意外終止。

但這裡的技巧是在合適的層面捕獲異常,以便你的程序要麼可以從異常中有意義地恢復並繼續下去,而不導致更深入的錯誤;要麼能夠為用戶提供明確的信息,包括引導他們從錯誤中恢復過來。如果你的方法無法勝任,那麼就不要處理異常,把它留到後面捕獲和在恰當的層面處理。

結論

經驗豐富的開發人員都知道,調試程序的最大難點不在於修復缺陷,而在於從海量的代碼中找出缺陷的藏身之處。只要遵循本文的三個原則,就能讓你的異常協助你跟蹤和消滅缺陷,使你的程序更加健壯,對用戶更加友好。

來源:ImportNew-鄭瑋


1.《單片機與嵌入式系統應用》12月電子刊新鮮出爐~

2.CPU和GPU擅長和不擅長的地方

3.Python是這樣調用matlab程序的!

4.寫了 15 年代碼,總結出提升 10 倍效率的三件事

5.電子愛好者向電子工程師進階,你需要做些什麼?

6.C++17 標準正式發布,以後開發者可更簡單地編寫和維護代碼

免責聲明:本文系網絡轉載,版權歸原作者所有。如涉及作品版權問題,請與我們聯繫,我們將根據您提供的版權證明材料確認版權並支付稿酬或者刪除內容。



相關焦點

  • JAVA異常及其異常處理方式
    前言異常處理異常是程序中的一些錯誤,但並不是所有的錯誤都是異常比如說,你的代碼少了一個分號,那麼運行出來結果是提示是錯誤 java.lang.Error;如果你用System.out.println(100/0),那麼你是因為你用0做了除數,會拋出 java.lang.ArithmeticException 的異常。
  • Java中的異常處理概念以及異常處理方法
    它具有易於使用、可自行定義異常類,處理拋出的異常同時又不會降低程序運行的速度等優點。因而在 Java 程序設計時,應 充分地利用 Java 的異常處理機制,以增進程序的穩定性及效率。2、簡單的異常範例    Java 本身已有相當好的機制來處理異常的發生。本節先來看看 Java 是如何處理異常的。
  • java NullPointerException異常處理
    java語言與其他程式語言不同,它有嚴格的異常處理機制,如果需要拋出異常的代碼塊中沒有捕獲異常,程序將不會成功編譯,換言之:未使用異常捕獲、異常捕獲方式不正確等,都將導致代碼不能被jvm編譯。java標準庫,內建了大量的異常處理類,這些類以Throwable 設計為最頂層類。
  • 基礎 | Java 中的異常捕獲及處理
    為了能夠及時有效地處理程序中的運行錯誤,Java 專門引入了異常類。Throwable 類是所有異常和錯誤的超類,下面有 Error 和 Exception 兩個子類分別表示錯誤和異常。我們不討論關於 Error 類型的異常處理,因為它們通常是災難性的致命錯誤,不是程序可以控制的。
  • Java 異常處理的誤區和經驗總結
    /#ibm-pcon在寫代碼的過程中,我們往往會忽略一些異常處理的基礎知識。本文旨在介紹 Java 異常的常見誤區和一些細節處理,包括異常的選擇、錯誤代碼的利用、處理多層次的異常、以及如何添加有效信息到異常等。本文著重介紹了 Java 異常選擇和使用中的一些誤區,希望各位讀者能夠熟練掌握異常處理的一些注意點和原則,注意總結和歸納。只有處理好了異常,才能提升開發人員的基本素養,提高系統的健壯性,提升用戶體驗,提高產品的價值。圖 1.
  • Java項目中常見的異常產生原因及處理辦法
    使用throw語句可以隨時拋出這種異常對象:throw new ArithmeticException(…);二、異常發生的原因有很多,通常包含以下幾大類:三、幾種常見的異常及其產生原因1、java.lang.NullPointerException(空指針異常)原因:這個異常大家肯定都經常遇到,異常的解釋是"程序遇上了空指針",即,調用了未經初始化的對象或者是不存在的對象
  • Java 中的異常和處理詳解
    Java提供了更加優秀的解決辦法:異常處理機制。異常處理機制能讓程序在異常發生時,按照代碼的預先設定的異常處理邏輯,針對性地處理異常,讓程序盡最大可能恢復正常並繼續執行,且保持代碼的清晰。javac在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。所以如果願意,我們可以編寫代碼處理(使用try…catch…finally)這樣的異常,也可以不處理。對於這些異常,我們應該修正代碼,而不是去通過異常處理器處理 。這樣的異常發生的原因多半是代碼寫的有問題。
  • Java【8】異常處理
    異常處理機制——當程序出現錯誤後,程序如何處理。具體來說,異常機制提供了程序退出的安全通道。當出現錯誤後,程序執行的流程發生改變,程序的控制權轉移到異常處理器。★ 掌握和編寫異常處理機製程序:找到異常出現的邏輯、使用完整的數據測試、形成有針對性的異常處理。
  • Java 異常處理的十個建議
    所以應該把exception列印到日誌中哦~三、不要用一個Exception捕捉所有可能的異常反例:public void test(){ try{ //…拋出 IOException 的代碼調用 //…拋出 SQLException 的代碼調用
  • Java 異常處理的 9 個最佳實踐
    在 Java 中處理異常並不是一件易事。新手覺得處理異常難以理解,甚至是資深開發者也會花上好幾個小時來討論是應該拋出拋異常還是處理異常。這就是為何大多數開發團隊都擁有一套自己的異常處理規範。如果你初進團隊,你也許會發現這些規範和你曾使用的規範大相逕庭。儘管如此,這裡還是有一些被大多數團隊所遵循的最佳實踐準則。
  • Java 異常處理的 10 個良心建議!
    所以應該把exception列印到日誌中哦~三、不要用一個Exception捕捉所有可能的異常public void test(){ try{ //…拋出 IOException 的代碼調用 //…拋出 SQLException 的代碼調用 }catch(Exception e){ //用基類 Exception
  • Java 中處理異常的 9 個最佳實踐
    以下為譯文:Java中的異常處理不是一個簡單的話題。初學者很難理解,甚至有經驗的開發人員也會花幾個小時來討論應該如何拋出或處理這些異常。這就是為什麼大多數開發團隊都有自己的異常處理的規則和方法。如果你是一個團隊的新手,你可能會驚訝於這些方法與你之前使用過的那些方法有多麼不同。然而,有幾種異常處理的最佳方法被大多數開發團隊所使用。
  • Java基礎入門之異常、異常分類、異常防護解析
    (三)java處理異常有兩種方式捕獲異常和拋出異常1.捕獲異常:當明確知道怎麼去處理這些異常時,採取捕獲的方式進行處理。2.拋出異常:當不知道怎麼去處理這些異常時,採取拋出的方式進行處理。Exception類表示異常類,表示本身可以處理錯誤,在開發程序中進行異常處理,針對它的子類。
  • Java編程中常見的異常
    java.lang.illegalargumentexception    這個異常的解釋是"方法的參數錯誤",很多j2me的類庫中的方法在一些情況下都會引發這樣的錯誤,比如音量調節方法中的音量參數如果寫成負數就會出現這個異常,再比如g.setcolor(int red,int green,int blue)這個方法中的三個值,如果有超過255的也會出現這個異常
  • Java異常面試問題
    異常是在程序執行期間可能發生的錯誤事件,並且會中斷它的正常流程。異常可能來自不同類型的情況,例如用戶輸入的錯誤數據,硬體故障,網絡連接故障等。每當執行java語句時發生任何錯誤,都會創建一個異常對象,然後JRE會嘗試查找異常處理程序來處理異常。
  • 為什麼不推薦使用try-catch-finally處理Java異常?
    「    本文的所有例子均在本地代碼運行完畢       基於JDK版本1.8,運行環境eclipse       本文類名:TryWithResources,下文的堆棧信息也以此為基礎    」在java開發中,一些網絡連結或者是文件資源都需要程式設計師去手動調用close方法關閉,比如InputStream、OutputStream和java.sql.Connection
  • Java中處理異常的9個最佳實踐,你必須要知道!
    對異常進行文檔說明當在方法上聲明拋出異常時,也需要進行文檔說明。和前面的一點一樣,都是為了給調用者提供儘可能多的信息,從而可以更好地避免/處理異常。異常處理的 10 個最佳實踐,這篇也推薦看下。在Javadoc中加入throws聲明,並且描述拋出異常的場景。
  • Java中處理異常的9條最佳實踐!快看你有沒有搞錯!
    Java中對於異常處理並不簡單。初學者很難去理解其中奧義,即使是有經驗的開發人員也可能會花幾個小時討論應該如何拋出或處理哪些異常。這就是為什麼大多數開發團隊對如何處理異常都會有自己的一套規則的原因。如果你是一個團隊中的新手,你可能會因這些規則和你以前使用過的有很大的不同而感到吃驚。不過,大多數團隊都會使用一些異常處理的最佳方法。
  • 代碼實踐 l 異常和錯誤的處理
    基於上面的優點像java,C#,swift這些強類型語言都有異常處理語法,其中java算是比較早的完善了異常處理的語法。但是各個語言異常處理的邏輯還是有細微的差別,這裡拿java和swift語言對比下各自的異常處理。
  • java.lang.NullPointerException -如何處理空指針異常?
    例如,最多創建一個類實例的示例方法是將其所有構造函數聲明為private,然後創建一個返回該類的唯一實例的公共方法:TestSingleton.javaimport java.util.UUID;class Singleton {