異常處理機(jī)制能讓程序在異常發(fā)生時,按照代碼的預(yù)先設(shè)定的異常處理邏輯,針對性地處理異常,讓程序盡最大可能恢復(fù)正常并繼續(xù)執(zhí)行,且保持代碼的清晰。
Java中的異??梢允呛瘮?shù)中的語句執(zhí)行時引發(fā)的,也可以是程序員通過throw 語句手動拋出的,只要在Java程序中產(chǎn)生了異常,就會用一個對應(yīng)類型的異常對象來封裝異常,JRE就會試圖尋找異常處理程序來處理異常。
Throwable類是Java異常類型的頂層父類,一個對象只有是 Throwable 類的(直接或者間接)實(shí)例,他才是一個異常對象,才能被異常處理機(jī)制識別。JDK中內(nèi)建了一些常用的異常類,我們也可以自定義異常。
Throwable又派生出Error類和Exception類。
錯誤:Error類以及他的子類的實(shí)例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現(xiàn)。因此,程序員應(yīng)該關(guān)注Exception為父類的分支下的各種異常類。
異常:Exception以及他的子類,代表程序運(yùn)行時發(fā)送的各種不期望發(fā)生的事件??梢灶A(yù)料的意外情況,可能并且應(yīng)該被捕獲,進(jìn)行相應(yīng)處理,可以被Java異常處理機(jī)制使用,是異常處理的核心。
總體上我們根據(jù)Javac對異常的處理要求,將異常類分為2類:
非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發(fā)現(xiàn)這樣的異常,不要求在程序處理這些異常。所以如果愿意,我們可以編寫代碼處理(使用try…catch…finally)這樣的異常,也可以不處理。對于這些異常,我們應(yīng)該修正代碼,而不是去通過異常處理器處理 。這樣的異常發(fā)生的原因多半是代碼寫的有問題。
檢查異常(checked exception):除了Error 和 RuntimeException的其它異常。javac強(qiáng)制要求程序員為這樣的異常做預(yù)備處理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch語句捕獲它并處理,要么用throws子句聲明拋出它,否則編譯不會通過。這樣的異常一般是由程序的運(yùn)行環(huán)境導(dǎo)致的。因?yàn)槌绦蚩赡鼙贿\(yùn)行在各種未知的環(huán)境下,而程序員無法干預(yù)用戶如何使用他編寫的程序,于是程序員就應(yīng)該為這樣的異常時刻準(zhǔn)備著。

在編寫代碼處理異常時,對于檢查異常,有2種不同的處理方式:使用try…catch…finally語句塊處理它。或者,在函數(shù)簽名中使用throws 聲明交給函數(shù)調(diào)用者caller去解決。
1)try…catch…finally語句塊:
try
{
???? //try塊中放可能發(fā)生異常的代碼。
???? //如果執(zhí)行完try且不發(fā)生異常,則接著去執(zhí)行finally塊和finally后面的代碼(如果有的話)。
???? //如果發(fā)生異常,則嘗試去匹配catch塊。
}
catch (SQLException SQLexception)
{
????//每一個catch塊用于捕獲并處理一個特定的異常,或者這異常類型的子類。Java7中可以將多個異常聲明在一個catch中。
????//catch后面的括號定義了異常類型和異常參數(shù)。如果異常與之匹配且是最先匹配到的,則虛擬機(jī)將使用這個catch塊來處理 異常。
????//在catch塊中可以使用這個塊的異常參數(shù)來獲取異常的相關(guān)信息。異常參數(shù)是這個catch塊中的局部變量,其它塊不能訪問。
????//如果當(dāng)前try塊中發(fā)生的異常在后續(xù)的所有catch中都沒捕獲到,則先去執(zhí)行finally,然后到這個函數(shù)的外部caller中去匹配異常處理器。
??? //如果try中沒有發(fā)生異常,則所有的catch塊將被忽略。
}
catch (Exception exception)
{
??? //...
}
finally
{
? ? //finally塊通常是可選的。
?? //無論異常是否發(fā)生,異常是否匹配被處理,finally都會執(zhí)行。
?? //一個try至少要有一個catch塊,否則, 至少要有1個finally塊。但是finally不是用來處理異常的,finally不會捕獲異常。
? //finally主要做一些清理工作,如流的關(guān)閉,數(shù)據(jù)庫連接的關(guān)閉等。
}
需要注意的地方
1、try塊中的局部變量和catch塊中的局部變量(包括異常變量),以及finally中的局部變量,他們之間不可共享使用。
2、每一個catch塊用于處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個匹配的catch會得到執(zhí)行。匹配時,不僅運(yùn)行精確匹配,也支持父類匹配,因此,如果同一個try塊下的多個catch異常類型有父子關(guān)系,應(yīng)該將子類異常放在前面,父類異常放在后面,這樣保證每個catch塊都有存在的意義。
3、java中,異常處理的任務(wù)就是將執(zhí)行控制流從異常發(fā)生的地方轉(zhuǎn)移到能夠處理這種異常的地方去。也就是說:當(dāng)一個函數(shù)的某條語句發(fā)生異常時,這條語句的后面的語句不會再執(zhí)行,它失去了焦點(diǎn)。執(zhí)行流跳轉(zhuǎn)到最近的匹配的異常處理catch代碼塊去執(zhí)行,異常被處理完后,執(zhí)行流會接著在“處理了這個異常的catch代碼塊”后面接著執(zhí)行。
2)throws 函數(shù)聲明
????? throws聲明:如果一個方法內(nèi)部的代碼會拋出檢查異常(checked exception),而方法自己又沒有完全處理掉,則javac保證你必須在方法的簽名上使用throws關(guān)鍵字聲明這些可能拋出的異常,否則編譯不通過。
throws是另一種處理異常的方式,它不同于try…catch…finally,throws僅僅是將函數(shù)中可能出現(xiàn)的異常向調(diào)用者聲明,而自己則不具體處理。
采取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常,或者說讓調(diào)用者處理更好,調(diào)用者需要為可能發(fā)生的異常負(fù)責(zé)。
public void foo() throws ExceptionType1, ExceptionType2, ExceptionTypeN
{
???? //foo內(nèi)部可以拋出 ExceptionType1 , ExceptionType2, ExceptionTypeN 類的異常,或者他們的子類的異常對象。
}
finally塊和return:
首先一個不容易理解的事實(shí):在 try塊中即便有return,break,continue等改變執(zhí)行流的語句,finally也會執(zhí)行。
也就是說:try…catch…finally中的return只要能執(zhí)行,就都執(zhí)行了,他們共同向同一個內(nèi)存地址(假設(shè)地址是0×80)寫入返回值,后執(zhí)行的將覆蓋先執(zhí)行的數(shù)據(jù),而真正被調(diào)用者取的返回值就是最后一次寫入的。
根據(jù)上述推斷下面結(jié)論就比較容易理解:
情況一:如果finally中有return語句,則會將try中的return語句”覆蓋“掉,直接執(zhí)行finally中的return語句,得到返回值,這樣便無法得到try之前保留好的返回值。
情況二:如果finally中沒有return語句,也沒有改變要返回值,則執(zhí)行完finally中的語句后,會接著執(zhí)行try中的return語句,返回之前保留的值。
情況三:如果finally中沒有return語句,但是改變了要返回的值,這里有點(diǎn)類似于引用傳遞和值傳遞的區(qū)別,分以下兩種情況:
? ? ? ? ?* 如果return的數(shù)據(jù)是基本數(shù)據(jù)類型,則在finally中對該基本數(shù)據(jù)的改變不起作用,try中的return語句依然會返回進(jìn)入finally塊之前保留的值。
? ? ? ? * 如果return的數(shù)據(jù)是引用數(shù)據(jù)類型,而在finally中對該引用數(shù)據(jù)類型的屬性值的改變起作用,try中的return語句返回的就是在finally中改變后的該屬性的值。
public class ExceptionTest {
? ? public static void main(String[] args)
? ? {
? ? ? ? ExceptionTest test = new ExceptionTest();
? ? ? ? System.out.println(test.fun());
? ? ? ? System.out.println(test.fun1());
? ? ? ? System.out.println(test.fun2());
? ? }
// 情況一:如果finally中有return語句,則會將try中的return語句”覆蓋“掉,直接執(zhí)行finally中的return語句,得到返回值,這樣便無法得到try之前保留好的返回值。
? ? public int fun()
? ? {
? ? ? ? int i = 10;
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? //doing something
? ? ? ? ? ? return i;
? ? ? ? }catch(Exception e){
? ? ? ? ? ? return i;
? ? ? ? }finally{
? ? ? ? ? ? i = 20;
? ? ? ? ? ? return i;
? ? ? ? }
? ? }
輸出結(jié)果:20
// 情況二:如果finally中沒有return語句,但是改變了要返回的值,這里有點(diǎn)類似于引用傳遞和值傳遞的區(qū)別,分以下兩種情況:
// 如果return的數(shù)據(jù)是基本數(shù)據(jù)類型,則在finally中對該基本數(shù)據(jù)的改變不起作用,try中的return語句依然會返回進(jìn)入finally塊之前保留的值。
? ? public int fun1()
? ? {
? ? ? ? int i = 10;
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? //doing something
? ? ? ? ? ? return i;
? ? ? ? }catch(Exception e){
? ? ? ? ? ? return i;
? ? ? ? }finally{
? ? ? ? ? ? i = 20;
? ? ? ? }
? ? }
輸出結(jié)果:10
// 如果return的數(shù)據(jù)是引用數(shù)據(jù)類型,而在finally中對該引用數(shù)據(jù)類型的屬性值的改變起作用,try中的return語句返回的就是在finally中改變后的該屬性的值。
? ? public StringBuilder fun2()
? ? {
? ? ? ? StringBuilder s = new StringBuilder("Hello");
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? //doing something
? ? ? ? ? ? s.append("Word");
? ? ? ? ? ? return s;
? ? ? ? }catch(Exception e){
? ? ? ? ? ? return s;
? ? ? ? }finally{
? ? ? ? ? ? s.append("finally");
? ? ? ? }
? ? }
}
輸出結(jié)果:HelloWordfinally
總結(jié)一下:其實(shí)return與finally并沒有明顯的誰強(qiáng)誰弱。在執(zhí)行時,是return語句先把返回值寫入但內(nèi)存中,然后停下來等待finally語句塊執(zhí)行完,return再執(zhí)行后面的一段。
建議:
* 不要在fianlly中使用return。
* 不要在finally中拋出異常。
* 減輕finally的任務(wù),不要在finally中做一些其它的事情,finally塊僅僅用來釋放資源是最合適的。
* 盡量將所有的return寫在函數(shù)的最后面,而不是try … catch … finally中。
? ? ? ?備注:在 finally 塊中拋出的任何異常都會覆蓋掉在其前面由 try 或者 catch 塊拋出異常。包含 return 語句的情形相似。
11條Java異常處理的最佳實(shí)踐
1)不要在catch語句塊中壓制異常
public class ExceptionExample {
??? public FileInputStreamtestMethod1(){
??????? File file = newFile("test.txt");
??????? FileInputStreamfileInputStream = null;
??????? try{
??????????? fileInputStream= new FileInputStream(file);
???????????fileInputStream.read();
??????? }catch (IOExceptione){????????
??????????? return null;
??????? }
??????? return fileInputStream;
??? }
??? public static void main(String[] args){
??????? ExceptionExampleinstance1 = new ExceptionExample();
???????instance1.testMethod1();
??? }
}???
在異常處理時進(jìn)行異常壓制是非常不好的編程習(xí)慣,上面的例子中,無論拋出什么異常都會被忽略,以至沒有留下任何問題線索。如果在這一層次不知道如何處理異常,最好將異常重新拋出,由上層決定如何處理異常。
public class ExceptionExample {
??? public FileInputStreamtestMethod1() throws IOException{
??????? File file = newFile("test.txt");
??????? FileInputStreamfileInputStream = null;
??????? try{
??????????? fileInputStream= new FileInputStream(file);
???????????fileInputStream.read();
??????? }catch (IOExceptione){????????
??????????? throw e;
??????? }
??????? returnfileInputStream;
??? }
??? public static voidmain(String[] args) throws IOException{
??????? ExceptionExampleinstance1 = new ExceptionExample();
???????instance1.testMethod1();
??? }
}
2)要在方法定義分句中定義具體的異常
按照public FileInputStream testMethod1() throws Exception{這種寫法,表示該方法會拋出所有受檢查異常,這不是一個良好的編程習(xí)慣。在這種情況下,我們最好拋出足夠具體的異常,以便調(diào)用者進(jìn)行合適的捕獲和處理,例如public FileInputStream testMethod1() throws IOException。
3)捕獲具體的異常
在調(diào)用其他模塊時,最好捕獲由該模塊拋出的具體的異常。如果某個被調(diào)用模塊拋出了多個異常,那么只捕獲這些異常的父類是不好的編程習(xí)慣。
例如,如果一個模塊拋出FileNotFoundException和IOException,那么調(diào)用這個模塊的代碼最好寫兩個catch語句塊分別捕獲這兩個異常,而不要只寫一個捕獲Exception的catch語句塊。
正確的寫法如下:
try {
?? //some statements
catch(FileNotFoundException e){
//handle here
}
catch(IOException e){
//handle here
}
你最好不要這么寫:
try {
?? //some statements
catch(Exception e){
//handle here
}
4)記得在finally語句塊中釋放資源
當(dāng)你在代碼中建立了數(shù)據(jù)庫連接、文件操作符或者其他需要被及時釋放的系統(tǒng)資源,如果你沒有及時釋放這些資源,會影響到系統(tǒng)的性能。
為了避免這種情況發(fā)生,可以使用Java 7的try(open the resources) {deal with resources}語句,如果你還是習(xí)慣這種老式寫法,則可以按照如下方式寫:
finally {
??? try {
??????? if (con != null) {
??????????? con.close();
??????? }
??????? if (stat != null) {
??????????? stat.close();
??????? }
??? } catch (SQLExceptionsqlee) {
???????sqlee.printStackTrace();
??? }
}
5)異常會影響性能
異常處理的性能成本非常高,每個Java程序員在開發(fā)時都應(yīng)牢記這句話。創(chuàng)建一個異常非常慢,拋出一個異常又會消耗1~5ms,當(dāng)一個異常在應(yīng)用的多個層級之間傳遞時,會拖累整個應(yīng)用的性能。
僅在異常情況下使用異常;
在可恢復(fù)的異常情況下使用異常;
盡管使用異常有利于Java開發(fā),但是在應(yīng)用中最好不要捕獲太多的調(diào)用棧,因?yàn)樵诤芏嗲闆r下都不需要打印調(diào)用棧就知道哪里出錯了。因此,異常消息應(yīng)該提供恰到好處的信息。
6)使用標(biāo)準(zhǔn)異常
如果使用內(nèi)建的異常可以解決問題,就不要定義自己的異常。Java API提供了上百種針對不同情況的異常類型,在開發(fā)中首先盡可能使用Java API提供的異常,如果標(biāo)準(zhǔn)的異常不能滿足你的要求,這時候創(chuàng)建自己的定制異常。盡可能得使用標(biāo)準(zhǔn)異常有利于新加入的開發(fā)者看懂項(xiàng)目代碼。
7)正確的包裝異常類型
當(dāng)需要在應(yīng)用重新拋出異常時,應(yīng)該正確得包裝原始異常,否則會丟失原始異常,例如下面的例子中:
import java.io.IOException;
public class HelloWorld{
???? public static voidmain(String []args) throws Exception{
??????? try{
? ??????????throw newIOException("IOException");???
??????? }catch (IOExceptione){
??????????? throw newExampleException1("Example Exception and " + e.getMessage());
??????? }
? ? ?}
}
class ExampleException1 extends Exception{
??? public ExampleException1(Strings, Throwable t){
??????? super(s,t);
??? }
??? publicExampleException1(String s){
??????? super(s);
??? }
}
這個程序的輸出為:
Exception in thread "main" ExampleException1: ExampleException and IOException?????????????????????????????????????????????????????????????????????????????????????????
??????? atHelloWorld.main(HelloWorld.java:8)??????????????????
這里發(fā)現(xiàn),IOException的調(diào)用棧已經(jīng)丟失了,因?yàn)槲覀冊赾atch語句塊中沒有正確包裝IOException。若將catch語句塊修改成下面這樣,這可以發(fā)現(xiàn)原始異常的調(diào)用棧也被打印出來了。
catch (IOException e){
??? throw newExampleException1("Example Exception",e);
}
這時候的輸出如下:
Exception in thread "main" ExampleException1: ExampleException????????????? ??????????????????????????????????????????????????????????????????????????????????
??????? atHelloWorld.main(HelloWorld.java:8)???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
Caused by: java.io.IOException: IOException?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??????? atHelloWorld.main(HelloWorld.java:6)?????????
8)避免在finally語句塊中拋出異常
try {
? method();? //here throws first exception
} finally {
? shutdown(); //If finallyblockthrew any exception the first exception will be lost forever
}
在上面的這個代碼片段中,finally代碼塊也可能再次拋出異常。如果同時拋出兩個異常,則第一個異常的調(diào)用棧會丟失。在finally語句塊中最好只做打印錯誤信息或者關(guān)閉資源等操作,避免在finally語句塊中再次拋出異常。
9)不要使用異??刂瞥绦虻牧鞒?/b>
不應(yīng)該使用異常控制應(yīng)用的執(zhí)行流程,例如,本應(yīng)該使用if語句進(jìn)行條件判斷的情況下,你卻使用異常處理,這是非常不好的習(xí)慣,會嚴(yán)重影響應(yīng)用的性能。
10)不要捕獲Throwable類
在應(yīng)用中不應(yīng)捕獲Throwable類,Error是Throwable類的子類,當(dāng)應(yīng)用拋出Errors的時候,一般都是不可恢復(fù)的情況。
11)為異常記錄合適的文檔
為應(yīng)用中定義的異常定義合適的文檔,如果你寫了一個自定義的異常卻沒有文檔,其他開發(fā)者會不清楚這個異常的含義,為你定義的異常配備對應(yīng)的文檔是一個非常好的習(xí)慣。
關(guān)于NoClassDefFoundError和ClassNotFoundException異常
java.lang.NoClassDefFoundError 和 java.lang.ClassNotFoundException 都是 Java 語言定義的標(biāo)準(zhǔn)異常。從異常類的名稱看似乎都跟類的定義找不到有關(guān),但是還是有些差異。我們先來看一下 java 規(guī)范中對這兩個異常的說明:
java.lang.NoClassDefFoundError:
出現(xiàn)這個異常,并不是說這個.class類文件不存在,而是類加載器試圖加載類的定義時卻找不到這個類的定義,實(shí)際上.class文件是存在的。
java.lang.ClassNotFoundException:
從規(guī)范說明看, java.lang.ClassNotFoundException 異常拋出的根本原因是類文件找不到,缺少了.class 文件,比如少引了某個 jar,解決方法通常需要檢查一下 classpath 下能不能找到包含缺失 .class 文件的 jar。
但是,很多人在碰到 java.lang.NoClassDefFoundError 異常時也會下意識的去檢查是不是缺少了.class 文件,比如 SO 上的這位提問者(java.lang.NoClassDefFoundError: Couldnot initialize class XXX)-- “明明 classpath 下有那個 jar 為什么還報這個異?!啊6鴮?shí)際上,這個異常的來源根本不是因?yàn)槿鄙?.class 文件。而碰到這個異常的解決辦法,一般需要檢查這個類定義中的初始化部分(如類屬性定義、static 塊等)的代碼是否有拋異常的可能。
如果是 static 塊,可以考慮在其中將異常捕獲并打印堆棧等,或者直接在對類進(jìn)行初始化調(diào)用(如 new Foobar())時作try? catch。
static{
try{
... your init code here
}catch(Throwable t){
LOG.error("Failure during static initialization", t);
}
}