原文鏈接:blog.ouyangsihai.cn >> 你所需要的java異常的處理和設計及深入理解,看這篇就差不多了
一. 異常的定義
在《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("不知道如何處理該異?;蛘吒静幌胩幚硭遣蛔鎏幚碛植缓线m,這是重新拋出異常交給上一級處理");
//重新拋出異常
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中釋放資源。這樣不僅會使得程序占用更少的資源,也會避免不必要的由于資源未釋放而發生的異常情況。
九.總結
總結非常簡單,不要為了使用異常而使用異常。異常是程序設計的一部分,對它的設計也要考究點。
參考資料
- http://www.cnblogs.com/dolphin0520/p/3769804.html
- https://www.cnblogs.com/focusj/archive/2011/12/26/2301524.html
文章有不當之處,歡迎指正,你也可以關注我的微信公眾號:
好好學java
,獲取優質學習資源。