文章摘要: 採用try…catch塊進行了異常捕獲處理1)對程式碼塊用try..catch進行異常捕獲處理
在《java程式設計思想》中這樣定義 異常:阻止當前方法或作用域繼續執行的問題。雖然java中有異常處理機制,但是要明確一點,決不應該用”正常”的態度來看待異常。絕對一點說異常就是某種意義上的錯誤,就是問題,它可能會導致程式失敗。之所以java要提出異常處理機制,就是要告訴開發人員,你的程式出現了不正常的情況,請注意。
記得當初學習java的時候,異常總是搞不太清楚,不知道這個異常是什麼意思,為什麼會有這個機制?但是隨著知識的積累逐漸也對異常有一點感覺了。舉一個例子來說明一下異常的用途。
public class Calculator { public int devide(int num1, int num2) { //判斷除數是否為0 if(num2 == 0) { throw new IllegalArgumentException("除數不能為零"); } return num1/num2; } }
看一下這個類中關於除運算的方法,如果你是新手你可能會直接返回計算結果,根本不去考慮什麼引數是否正確,是否合法(當然可以原諒,誰都是這樣過來的)。但是我們應儘可能的考慮周全,把可能導致程式失敗的”苗頭”扼殺在搖籃中,所以進行引數的合法性檢查就很有必要了。其中執行引數檢查拋出來的那個引數非法異常,這就屬於這個方法的不正常情況。正常情況下我們會正確的使用計算器,但是不排除粗心大意把除數賦值為0。如果你之前沒有考慮到這種情況,並且恰巧使用者數學基礎不好,那麼你完了。但是如果你之前考慮到了這種情況,那麼很顯然錯誤已在你的掌控之中。
二. 異常掃盲行動
今天和別人聊天時看到一個笑話: 世界上最真情的相依,是你在try我在catch 。無論你發神馬脾氣,我都默默承受,靜靜處理。 大多數新手對java異常的感覺就是:try…catch…。沒錯,這是用的最多的,也是最實用的。我的感覺就是:java異常是從”try…catch…”走來。
首先來熟悉一下java的異常體系:
Throwable
類是 Java 語言中所有錯誤或異常的超類(這就是一切皆可拋的東西)。它有兩個子類: Error
和 Exception
。
Error
:用於指示合理的應用程式不應該試圖捕獲的嚴重問題。這種情況是很大的問題,大到你不能處理了,所以聽之任之就行了,你不用管它。比如說 VirtualMachineError
:當 Java 虛擬機器崩潰或用盡了它繼續操作所需的資源時,丟擲該錯誤。好吧,就算這個異常的存在了,那麼應該何時,如何處理它呢??交給JVM吧,沒有比它更專業的了。
Exception
:它指出了合理的應用程式想要捕獲的條件。Exception又分為兩類:一種是 CheckedException
,一種是 UncheckedException
。這兩種Exception的區別主要是CheckedException需要用try…catch…顯示的捕獲,而UncheckedException不需要捕獲。通常UncheckedException又叫做 RuntimeException
。《effective java》指出:對於可恢復的條件使用被檢查的異常(CheckedException),對於程式錯誤(言外之意不可恢復,大錯已經釀成)使用執行時異常(RuntimeException)。
我們常見的 RuntimeExcepiton
有 IllegalArgumentException
、 IllegalStateException
、 NullPointerException
、 IndexOutOfBoundsException
等等。對於那些CheckedException就不勝枚舉了,我們在編寫程式過程中try…catch…捕捉的異常都是 CheckedException
。io包中的IOException及其子類,這些都是CheckedException。
三、java中異常如何處理
在Java中如果需要處理異常,必須先對異常進行捕獲,然後再對異常情況進行處理。如何對可能發生異常的程式碼進行異常捕獲和處理呢?使用try和catch關鍵字即可,如下面一段程式碼所示:
try { File file = new File("d:/a.txt"); if(!file.exists()) file.createNewFile(); } catch (IOException e) { // TODO: handle exception }
被try塊包圍的程式碼說明這段程式碼可能會發生異常,一旦發生異常,異常便會被catch捕獲到,然後需要在catch塊中進行異常處理。
這是一種處理異常的方式。在Java中還提供了另一種異常處理方式即丟擲異常,顧名思義,也就是說一旦發生異常,我把這個異常拋出去,讓呼叫者去進行處理,自己不進行具體的處理,此時需要用到throw和throws關鍵字。
下面看一個示例:
public class Main { public static void main(String[] args) { try { createFile(); } catch (Exception e) { // TODO: handle exception } } public static void createFile() throws IOException{ File file = new File("d:/a.txt"); if(!file.exists()) file.createNewFile(); } }
這段程式碼和上面一段程式碼的區別是,在實際的createFile方法中並沒有捕獲異常,而是用throws關鍵字宣告丟擲異常,即告知這個方法的呼叫者此方法可能會丟擲IOException。那麼在main方法中呼叫createFile方法的時候,採用try…catch塊進行了異常捕獲處理。
當然還可以採用throw關鍵字手動來丟擲異常物件。下面看一個例子:
public class Main { public static void main(String[] args) { try { int[] data = new int[]{1,2,3}; System.out.println(getDataByIndex(-1,data)); } catch (Exception e) { System.out.println(e.getMessage()); } } public static int getDataByIndex(int index,int[] data) { if(index<0||index>=data.length) throw new ArrayIndexOutOfBoundsException("陣列下標越界"); return data[index]; } }
然後在catch塊中進行捕獲。
也就說在Java中進行異常處理的話,對於可能會發生異常的程式碼,可以選擇三種方法來進行異常處理:
1)對程式碼塊用try..catch進行異常捕獲處理;
2)在 該程式碼的方法體外用throws進行丟擲宣告,告知此方法的呼叫者這段程式碼可能會出現這些異常,你需要謹慎處理。此時有兩種情況:
如果宣告丟擲的異常是非執行時異常,此方法的呼叫者必須顯示地用try..catch塊進行捕獲或者繼續向上層丟擲異常。
如果宣告丟擲的異常是執行時異常,此方法的呼叫者可以選擇地進行異常捕獲處理。
3)在程式碼塊用throw手動丟擲一個異常物件,此時也有兩種情況,跟2)中的類似:
如果丟擲的異常物件是非執行時異常,此方法的呼叫者必須顯示地用try..catch塊進行捕獲或者繼續向上層丟擲異常。
如果丟擲的異常物件是執行時異常,此方法的呼叫者可以選擇地進行異常捕獲處理。
(如果最終將異常拋給main方法,則相當於交給jvm自動處理,此時jvm會簡單地列印異常資訊)
四.深刻理解try,catch,finally,throws,throw五個關鍵字
下面我們來看一下異常機制中五個關鍵字的用法以及需要注意的地方。
1.try,catch,finally
try關鍵字用來包圍可能會出現異常的邏輯程式碼,它單獨無法使用,必須配合catch或者finally使用。Java編譯器允許的組合使用形式只有以下三種形式:
try...catch...; try....finally......; try....catch...finally...
當然catch塊可以有多個,注意try塊只能有一個,finally塊是可選的(但是最多隻能有一個finally塊)。
三個塊執行的順序為try—>catch—>finally。
當然如果沒有發生異常,則catch塊不會執行。但是finally塊無論在什麼情況下都是會執行的(這點要非常注意,因此部分情況下,都會將釋放資源的操作放在finally塊中進行)。
在有多個catch塊的時候,是按照catch塊的先後順序進行匹配的,一旦異常型別被一個catch塊匹配,則不會與後面的catch塊進行匹配。
在使用try..catch..finally塊的時候,注意千萬不要在finally塊中使用return,因為finally中的return會覆蓋已有的返回值。下面看一個例子:
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class Main { public static void main(String[] args) { String str = new Main().openFile(); System.out.println(str); } public String openFile() { try { FileInputStream inputStream = new FileInputStream("d:/a.txt"); int ch = inputStream.read(); System.out.println("aaa"); return "step1"; } catch (FileNotFoundException e) { System.out.println("file not found"); return "step2"; }catch (IOException e) { System.out.println("io exception"); return "step3"; }finally{ System.out.println("finally block"); //return "finally"; } } }
這段程式的輸出結果為:
可以看出,在try塊中發生FileNotFoundException之後,就跳到第一個catch塊,列印”file not found”資訊,並將”step2″賦值給返回值,然後執行finally塊,最後將返回值返回。
從這個例子說明,無論try塊或者catch塊中是否包含return語句,都會執行finally塊。
如果將這個程式稍微修改一下,將finally塊中的 return語句註釋去掉 ,執行結果是:
最後列印出的是”finally”,返回值被重新覆蓋了。
因此如果方法有返回值,切忌不要再finally中使用return,這樣會使得程式結構變得混亂。
2.throws和thow關鍵字
1)throws出現在方法的宣告中,表示該方法可能會丟擲的異常,然後交給上層呼叫它的方法程式處理,允許throws後面跟著多個異常型別;
2)一般會用於程式出現某種邏輯時程式設計師主動丟擲某種特定型別的異常。throw只會出現在方法體中,當方法在執行過程中遇到異常情況時,將異常資訊封裝為異常物件,然後throw出去。throw關鍵字的一個非常重要的作用就是 異常型別的轉換(會在後面闡述道)。
throws表示出現異常的一種可能性,並不一定會發生這些異常;throw則是丟擲了異常,執行throw則一定丟擲了某種異常物件。兩者都是消極處理異常的方式(這裏的消極並不是說這種方式不好),只是丟擲或者可能丟擲異常,但是不會由方法去處理異常,真正的處理異常由此方法的上層呼叫處理。
五.在類繼承的時候,方法覆蓋時如何進行異常丟擲宣告
本小節討論子類重寫父類方法的時候,如何確定異常丟擲宣告的型別。下面是三點原則:
1)父類的方法沒有宣告異常,子類在重寫該方法的時候不能宣告異常;
2)如果父類的方法宣告一個異常exception1,則子類在重寫該方法的時候宣告的異常不能是exception1的父類;
3)如果父類的方法宣告的異常型別只有非執行時異常(執行時異常),則子類在重寫該方法的時候宣告的異常也只能有非執行時異常(執行時異常),不能含有執行時異常(非執行時異常)。
六. 異常的深入理解
在異常的使用這一部分主要是演示程式碼,都是我們平常寫程式碼的過程中會遇到的(當然只是一小部分),拋磚引玉嗎!
例1. 這個例子主要通過兩個方法對比來演示一下有了異常以後程式碼的執行流程。
public static void testException1() { int[] ints = new int[] { 1, 2, 3, 4 }; System.out.println("異常出現前"); try { System.out.println(ints[4]); System.out.println("我還有幸執行到嗎");// 發生異常以後,後面的程式碼不能被執行 } catch (IndexOutOfBoundsException e) { System.out.println("陣列越界錯誤"); } System.out.println("異常出現後"); } /*output: 異常出現前 陣列越界錯誤 4 異常出現後 */
public static void testException2() { int[] ints = new int[] { 1, 2, 3, 4 }; System.out.println("異常出現前"); System.out.println(ints[4]); System.out.println("我還有幸執行到嗎");// 發生異常以後,他後面的程式碼不能被執行 }
首先指出例子中的不足之處, IndexOutofBoundsException
是一個非受檢異常,所以不用try…catch…顯示捕捉,但是我的目的是對同一個異常用不同的處理方式,看它會有什麼不同的而結果(這裏也就只能用它將就一下了)。異常出現時第一個方法只是跳出了try塊,但是它後面的程式碼會照樣執行的。但是第二種就不一樣了直接跳出了方法,比較強硬。從第一個方法中我們看到, try...catch...
是一種”事務性”的保障,它的目的是保證程式在異常的情況下執行完畢,同時它還會告知程式設計師程式中出錯的詳細資訊(這種詳細資訊有時要依賴於程式設計師設計)。
例2. 重新丟擲異常
public class Rethrow { public static void readFile(String file) throws FileNotFoundException { try { BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); } catch (FileNotFoundException e) { e.printStackTrace(); System.err.println("不知道如何處理該異常或者根本不想處理它,但是不做處理又不合適,這是重新丟擲異常交給上一級處理"); //重新丟擲異常 throw e; } } public static void printFile(String file) { try { readFile(file); } catch (FileNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { printFile("D:/file"); } }
**異常的本意是好的,讓我們試圖修復程式,但是現實中我們修復的機率很小,我們很多時候就是用它來記錄出錯的資訊。如果你厭倦了不停的處理異常,重新丟擲異常對你來說可能是一個很好的解脫。原封不動的把這個異常拋給上一級,拋給呼叫這個方法的人,讓他來費腦筋吧。**這樣看來,java異常(當然指的是受檢異常)又給我們平添很多麻煩,儘管它的出發點是好的。
例3. 異常鏈的使用及異常丟失
定義三個異常類: ExceptionA,ExceptionB,ExceptionC
public class ExceptionA extends Exception { public ExceptionA(String str) { super(); } } public class ExceptionB extends ExceptionA { public ExceptionB(String str) { super(str); } } public class ExceptionC extends ExceptionA { public ExceptionC(String str) { super(str); } }
異常丟失的情況:
public class NeverCaught { static void f() throws ExceptionB{ throw new ExceptionB("exception b"); } static void g() throws ExceptionC { try { f(); } catch (ExceptionB e) { ExceptionC c = new ExceptionC("exception a"); throw c; } } public static void main(String[] args) { try { g(); } catch (ExceptionC e) { e.printStackTrace(); } } } /* exception.ExceptionC at exception.NeverCaught.g(NeverCaught.java:12) at exception.NeverCaught.main(NeverCaught.java:19) */
為什麼只是列印出來了ExceptionC而沒有列印出ExceptionB呢?這個還是自己分析一下吧!
上面的情況相當於少了一種異常,這在我們排錯的過程中非常的不利。那我們遇到上面的情況應該怎麼辦呢?這就是異常鏈的用武之地:儲存異常資訊,在丟擲另外一個異常的同時不丟失原來的異常。
public class NeverCaught { static void f() throws ExceptionB{ throw new ExceptionB("exception b"); } static void g() throws ExceptionC { try { f(); } catch (ExceptionB e) { ExceptionC c = new ExceptionC("exception a"); //異常連 c.initCause(e); throw c; } } public static void main(String[] args) { try { g(); } catch (ExceptionC e) { e.printStackTrace(); } } } /* exception.ExceptionC at exception.NeverCaught.g(NeverCaught.java:12) at exception.NeverCaught.main(NeverCaught.java:21) Caused by: exception.ExceptionB at exception.NeverCaught.f(NeverCaught.java:5) at exception.NeverCaught.g(NeverCaught.java:10) ... 1 more */
這個異常鏈的特性是所有異常均具備的,因為這個 initCause()
方法是從 Throwable
繼承的。
例4. 清理工作
清理工作對於我們來說是必不可少的,因為如果一些消耗資源的操作,比如IO,JDBC。如果我們用完以後沒有及時正確的關閉,那後果會很嚴重,這意味著記憶體洩露。異常的出現要求我們必須設計一種機制不論什麼情況下,資源都能及時正確的清理。這就是finally。
public void readFile(String file) { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader( new FileInputStream(file))); // do some other work } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } }
例子非常的簡單,是一個讀取檔案的例子。這樣的例子在JDBC操作中也非常的常見。(所以,我覺得對於資源的及時正確清理是一個程式設計師的基本素質之一。)
Try…finally結構也是保證資源正確關閉的一個手段。如果你不清楚程式碼執行過程中會發生什麼異常情況會導致資源不能得到清理,那麼你就用try對這段”可疑”程式碼進行包裝,然後在finally中進行資源的清理。舉一個例子:
public void readFile() { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader( new FileInputStream("file"))); // do some other work //close reader reader.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
我們注意一下這個方法和上一個方法的區別,下一個人可能習慣更好一點,及早的關閉reader。但是往往事與願違,因為在reader.close()以前異常隨時可能發生,這樣的程式碼結構不能預防任何異常的出現。因為程式會在異常出現的地方跳出,後面的程式碼不能執行(這在上面應經用例項證明過)。這時我們就可以用try…finally來改造:
public void readFile() { BufferedReader reader = null; try { try { reader = new BufferedReader(new InputStreamReader( new FileInputStream("file"))); // do some other work // close reader } finally { reader.close(); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
及早的關閉資源是一種良好的行為,因為時間越長你忘記關閉的可能性越大。這樣在配合上try…finally就保證萬無一失了(不要嫌麻煩,java就是這麼中規中矩)。
再說一種情況,假如我想在構造方法中開啟一個檔案或者建立一個JDBC連線,因為我們要在其他的方法中使用這個資源,所以不能在構造方法中及早的將這個資源關閉。那我們是不是就沒轍了呢?答案是否定的。看一下下面的例子:
public class ResourceInConstructor { BufferedReader reader = null; public ResourceInConstructor() { try { reader = new BufferedReader(new InputStreamReader(new FileInputStream(""))); } catch (FileNotFoundException e) { e.printStackTrace(); } } public void readFile() { try { while(reader.readLine()!=null) { //do some work } } catch (IOException e) { e.printStackTrace(); } } public void dispose() { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } }
這一部分講的多了一點,但是異常確實是看起來容易用起來難的東西呀,java中還是有好多的東西需要深挖的。
七. 異常的誤用
對於異常的誤用著實很常見,上一部分中已經列舉了幾個,大家仔細的看一下。下面再說兩個其他的。
例1.用一個Exception來捕捉所有的異常,頗有”一夫當關萬夫莫開”的氣魄。不過這也是最傻的行為。
public void readFile(String file) { BufferedReader reader = null; Connection conn = null; try { reader = new BufferedReader(new InputStreamReader( new FileInputStream(file))); // do some other work conn = DriverManager.getConnection(""); //... } catch (Exception e) { e.printStackTrace(); } finally { try { reader.close(); conn.close(); } catch (Exception e) { e.printStackTrace(); } } }
從異常角度來說這樣嚴格的程式確實是萬無一失,所有的異常都能捕獲。但是站在程式設計人員的角度,萬一這個程式出錯了我們該如何分辨是到底是那引起的呢,IO還是JDBC…所以,這種寫法很值得當做一個反例。大家不要以為這種做法很幼稚,傻子纔會做。我在公司實習時確實看見了類似的情況:只不過是人家沒有用Exception而是用了Throwable。
例2.這裏就不舉例子了,上面的程式都是反例。異常是程式處理意外情況的機制,當程式發生意外時,我們需要儘可能多的得到意外的資訊,包括髮生的位置,描述,原因等等。這些都是我們解決問題的線索。但是上面的例子都只是簡單的printStackTrace()。如果我們自己寫程式碼,就要儘可能多的對這個異常進行描述。比如說為什麼會出現這個異常,什麼情況下會發生這個異常。如果傳入方法的引數不正確,告知什麼樣的引數是合法的引數,或者給出一個sample。
例3.將try block寫的簡短,不要所有的東西都扔在這裏,我們儘可能的分析出到底哪幾行程式可能出現異常,只是對可能出現異常的程式碼進行try。儘量為每一個異常寫一個try…catch,避免異常丟失。在IO操作中,一個IOException也具有”一夫當關萬夫莫開”的氣魄。
八.異常處理和設計的幾個建議
以下是根據前人總結的一些異常處理的建議:
1.只在必要使用異常的地方纔使用異常,不要用異常去控制程式的流程
謹慎地使用異常,異常捕獲的代價非常高昂,異常使用過多會嚴重影響程式的效能。如果在程式中能夠用if語句和Boolean變數來進行邏輯判斷,那麼儘量減少異常的使用,從而避免不必要的異常捕獲和處理。比如下面這段經典的程式:
public void useExceptionsForFlowControl() { try { while (true) { increaseCount(); } } catch (MaximumCountReachedException ex) { } //Continue execution } public void increaseCount() throws MaximumCountReachedException { if (count >= 5000) throw new MaximumCountReachedException(); }
上邊的useExceptionsForFlowControl()用一個無限迴圈來增加count直到丟擲異常,這種做法並沒有說讓程式碼不易讀,而是使得程式執行效率降低。
2.切忌使用空catch塊
在捕獲了異常之後什麼都不做,相當於忽略了這個異常。千萬不要使用空的catch塊,空的catch塊意味著你在程式中隱藏了錯誤和異常,並且很可能導致程式出現不可控的執行結果。如果你非常肯定捕獲到的異常不會以任何方式對程式造成影響,最好用Log日誌將該異常進行記錄,以便日後方便更新和維護。
3.檢查異常和非檢查異常的選擇
一旦你決定丟擲異常,你就要決定丟擲什麼異常。這裏面的主要問題就是丟擲檢查異常還是非檢查異常。
檢查異常導致了太多的try…catch程式碼,可能有很多檢查異常對開發人員來說是無法合理地進行處理的,比如SQLException,而開發人員卻不得不去進行try…catch,這樣就會導致經常出現這樣一種情況:邏輯程式碼只有很少的幾行,而進行異常捕獲和處理的程式碼卻有很多行。這樣不僅導致邏輯程式碼閱讀起來晦澀難懂,而且降低了程式的效能。
我個人建議儘量避免檢查異常的使用,如果確實該異常情況的出現很普遍,需要提醒呼叫者注意處理的話,就使用檢查異常;否則使用非檢查異常。
因此,在一般情況下,我覺得儘量將檢查異常轉變為非檢查異常交給上層處理。
4.注意catch塊的順序
不要把上層類的異常放在最前面的catch塊。比如下面這段程式碼:
try { FileInputStream inputStream = new FileInputStream("d:/a.txt"); int ch = inputStream.read(); System.out.println("aaa"); return "step1"; } catch (IOException e) { System.out.println("io exception"); return "step2"; }catch (FileNotFoundException e) { System.out.println("file not found"); return "step3"; }finally{ System.out.println("finally block"); //return "finally"; }
第二個catch的FileNotFoundException將永遠不會被捕獲到,因為FileNotFoundException是IOException的子類。
5.不要將提供給使用者看的資訊放在異常資訊裡
比如下面這段程式碼:
public class Main { public static void main(String[] args) { try { String user = null; String pwd = null; login(user,pwd); } catch (Exception e) { System.out.println(e.getMessage()); } } public static void login(String user,String pwd) { if(user==null||pwd==null) throw new NullPointerException("使用者名稱或者密碼為空"); //... } }
展示給使用者錯誤提示資訊最好不要跟程式混淆一起,比較好的方式是將所有錯誤提示資訊放在一個配置檔案中統一管理。
6.避免多次在日誌資訊中記錄同一個異常
只在異常最開始發生的地方進行日誌資訊記錄。很多情況下異常都是層層向上跑出的,如果在每次向上丟擲的時候,都Log到日誌系統中,則會導致無從查詢異常發生的根源。
7. 異常處理儘量放在高層進行
儘量將異常統一拋給上層呼叫者,由上層呼叫者統一之時如何進行處理。如果在每個出現異常的地方都直接進行處理,會導致程式異常處理流程混亂,不利於後期維護和異常錯誤排查。由上層統一進行處理會使得整個程式的流程清晰易懂。
8. 在finally中釋放資源
如果有使用檔案讀取、網路操作以及資料庫操作等,記得在finally中釋放資源。這樣不僅會使得程式佔用更少的資源,也會避免不必要的由於資源未釋放而發生的異常情況。