您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關Java中如何對異常進行處理,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
異常及異常處理
什么是異常?總結為一句話就是:程序在執行過程中產生的異常情況。當某些事情出現了錯誤異常就會發生,比如打開一個并不存在的文件、嘗試在一個為null的對象上調用方法等等,都會發生異常。
異常是不可預知的,可是一旦它發生了就需要進行異常處理,可謂知錯就改善莫大焉!異常處理是一種錯誤處理機制,如果你不對異常做任何處理,異常將會導致應用程序崩潰。
一旦你選擇了進行處理異常,也就意味著你承認問題的發生,采用必要要的措施去讓應用程序從錯誤中恢復,從而讓業務繼續進行,阻止應用程序崩潰。
實際上異常處理并不是處理問題的唯一一種方式,如今的高級語言一般都有異常處理機制,但比較古老的如C語言是通過返回錯誤碼的方式來處理異常的。比如數組越界比較常用的返回值是-1。
這種方式的優點是代碼邏輯易于推理,沒有中斷和代碼跳轉。另一方面,這種處理方式鼓勵函數的調用者總是檢查返回的錯誤碼。但是這種檢查容易造成代碼污染,導致代碼的可讀性和可維護性降低。
錯誤代碼的另一個嚴重的缺點是缺乏上下文信息,你可能知道錯誤碼“-5”代表找不到文件,但究竟找不到哪個文件呢!錯誤碼就無法表述了。
錯誤代碼一般用于面向過程的語言,對面向對象的高級語言,有些場景是無能為力的,比如構造函數異常,是無法返回錯誤碼的。
異常處理
當異常被拋出時,應用程序的流程就會被中斷,如果沒能及時處理異常,應用程序將崩潰。用戶將看到異常信息,但那些信息大多他們是看不懂的,這將是一個很糟糕的用戶體驗,實際上異常信息還可以包裝成非常友好的提示。
所以必須進行異常處理,哪怕是為了提高用戶體驗、記錄問題日志、優雅地退出應用程序等。
我們可以使用代碼塊(try...catch...)來進行異常處理,當發生異常,通過try執行代碼,如果發生異常,應用程序的流程將轉移到catch中,catch捕捉到異常并進行必要的處理。
以上表述的異常處理原理對初學者依然比較抽象,我們來舉個例子
package com.zqf;
public class App
{
public static void main(String[] args){
System.out.println("First line");
System.out.println("Second line");
System.out.println("Third line");
//初始化具有3個元素的素組
int[] myIntArray = new int[]{1, 2, 3};
print4thItemInArray(myIntArray);
System.out.println("Fourth line");
System.out.println("Fith line");
}
private static void print4thItemInArray(int[] arr) {
//獲取第4個(下標3)元素,因為沒有所以拋出異常
System.out.println(arr[3]);
System.out.println("Fourth element successfully displayed!");
}
}
分析下這個程序,在main中初始化有3個元素的數組,把這個數組傳遞給私有方法print4thItemInArray,在print4thItemInArray中試圖獲取數組的第4個元素,由于沒有第4個元素將拋出“ArrayIndexOutOfBoundsException”異常,應用程序只會打印到“Third line”。
執行應用輸出結果如下
First line
Second line
Third line
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at com.zqf.App.print4thItemInArray(App.java:20)
at com.zqf.App.main(App.java:14)
Process finished with exit code 1
現在我們修改下,增加異常處理
package com.zqf;
public class App {
public static void main(String[] args) {
System.out.println("First line");
System.out.println("Second line");
System.out.println("Third line");
//初始化具有3個元素的素組
int[] myIntArray = new int[]{1, 2, 3};
try {//捕捉異常
print4thItemInArray(myIntArray);
}catch (ArrayIndexOutOfBoundsException ex){//異常處理
System.out.println("Have no four items!");
}
System.out.println("Fourth line");
System.out.println("Fith line");
}
private static void print4thItemInArray(int[] arr) {
//獲取第4個(下標3)元素,因為沒有所以拋出異常
System.out.println(arr[3]);
System.out.println("Fourth element successfully displayed!");
}
}
現在運行看看輸出
First line
Second line
Third line
Have no four items!
Fourth line
Fith line
實際上這次的異常依然會發生,因為第4個元素的確不存在,所以在"Fourth element successfully displayed!"輸出之前就拋出了異常,中斷執行流程,但流程跳轉到catch語句塊了,catch只打印了一條“Have no four items”,繼續向下執行。
Java異常體系
在Java中,所有的異常都有一個共同的祖先Throwable,它有2個子類:Exception(異常)和Error(錯誤),它們又各自有大量的子類。Exception(異常)和Error(錯誤)的共性和區別:兩者都可以被捕捉,但前者可以被應用程序本身處理,后者是嚴重的,是無法恢復處理的。
最佳實踐
我們經常在try語句塊使用資源,比如InputStream,使用完后需要關閉。經常犯的錯誤是在try語句塊中關閉資源。如
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// 使用inputStream讀取文件
// 不要這樣做
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
這種方式看似非常完美,不會有異常拋出,所有的語句在try中執行,關閉IputStream釋放資源。但試想一下:如果在“inputStream.close()”語句之前就拋出異常,會怎樣呢?正常的流程會被中斷并跳轉,導致InputStream根本沒關閉。
因此,應該把清理資源的代碼放在finally或try-with-resource語句中。不管是正常執行完try語句塊,還是異常處理完畢,都會執行finally語句塊,而你需要確保在finally關閉所有打開的資源。
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// 使用inputStream讀取文件
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
log.error(e);
}
}
}
}
在JDK7引入了try-with-resource的語法,簡單來說當一個資源對象(如InputSteam對象)實現了AutoCloseable接口,那么就可以在try關鍵字后的括號里創建實例,當try-catch語句塊執行完畢后,會自動關閉資源,代碼也會簡潔許多。如下
public void doNotCloseResourceInTry() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file)) {
// 使用inputStream讀取文件
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
你拋出的異常越具體越好,不熟悉你代碼的同事或者幾個月之后的你,可能需要調用你這些方法并進行異常處理,所以盡可能多的提供信息,讓你的API更容易理解,比如能用NumberFormatException就不要用 IllegalArgumentException,絕對避免直接使用不具體的Exception類。
//不建議
public void doNotDoThis() throws Exception { ... }
//建議
public void doThis() throws NumberFormatException { ... }
只要你在方法聲明異常,就需要做好Javadoc的注釋。這點和上一條最佳實踐有相同的目標:提供給調用者盡可能多的信息,便于避免異常或進行異常處理。所以請確保你在Javadoc中添加了"@throws"聲明,并且描述了造成異常的情況。
/**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException if ... happens
*/
public void doSomething(String input) throws MyBusinessException { ... }
這條最佳實踐和前面兩條有點相似,但這條提供的信息不單是給方法調用者看的,而更多的是為了給記錄日志或監控工具提供的,便于排查異常。實際上一般的異常類名就已經描述了問題的類型,你不必提供大量的附加信息,簡潔凝練即可。比如NumberFormatException,當java.lang.Long構造函數拋出異常時會提供一句簡短且清晰的文本來描述。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}
NumberFormatException 的名字就已經告訴你異常的種類,它攜帶的信息僅告訴你提供的字符串會導致異常,但如果異常名字不能表達異常種類,就需要提供更多的信息。上述的異常信息如下
17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"
如果你仔細看下JDK的源碼,就會清楚java.lang.Long在構造器中做了各種校驗,當某些校驗失敗會調用NumberFormatException.forInputString,而靜態方法forInputString會把java.lang.Long的構造參數格式化后再構造一個新的NumberFormatException實例并拋出
/**
* Factory method for making a <code>NumberFormatException</code>
* given the specified input which caused the error.
*
* @param s the input causing the error
*/
static NumberFormatException forInputString(String s) {
return new NumberFormatException("For input string: \"" + s + "\"");
}
很多IDE都會幫助你進行最佳實踐,如果你先捕捉父類異常再捕捉子類異常,它們會告訴你后面的代碼不可到達或者警告已經被捕捉,因為是按照catch在在代碼中順序執行的。
所以如果先捕捉IllegalArgumentException,將不能捕捉到其子類NumberFormatException,因此最佳時間是總是先捕捉更多信息的異常(子類),再捕捉父類。如
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
從Java異常體系的圖中可知,Throwable是所有異常(Exception)和錯誤(Error)的祖先,Throwable是可以被捕捉,但請不要捕捉。如果你捕捉了Throwable,那么不僅僅是捕捉了異常,還捕捉了錯誤。但錯誤是無法恢復,它是被JVM拋出的嚴重錯誤,應用程序對這類錯誤是無能為力的。
//不要捕捉Throwable
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
你是否記得曾幾何時,在分析bug時遇到代碼只執行了前半部分,但卻不知為何。有些開發者經常捕捉了異常,但憑經驗認為異常決定不可能發生,導致沒有做異常處理。
public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen,I'm sure!!
}
}
實際上在大多數情況下它都發生了,因為隨著時間和業務邏輯的變更,try代碼塊的內容變更了,導致了異常發生,而你的自信不僅害了你也害了后來人。建議catch中至少要留一條日志,來告知異常問題,方便排查。
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: "
+ e + ",but I get a mistake!");
}
}
“不要在僅僅記錄日志后向上拋出異常”,這是最佳實踐中最容易被忽視的一條。你會發現在大量的代碼片段,甚至類庫中經常捕捉異常、記錄日志,然后拋出異常。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
直觀的感覺是記錄異常,然后拋出異常讓調用者可以恰當的處理,但同一個異常多處日志記錄,會讓人迷惑,請參考第4條最佳實踐:簡潔凝練。
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
如果你真的需要給調用者提供更多信息,可以參考下一條最佳實踐:包裝異常。
比較可取的做法是捕捉到標準異常,根據實際業務自定義包裝異常再向上拋出。在包裝異常時通常把原始異常作為構造參數傳進來,否則會丟失棧的跟蹤信息,造成分析困難。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
關于Java中如何對異常進行處理就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。