什麼是異常
從事Java
開發的小夥伴對於“異常”應該不陌生,因爲每天都會遇到不少異常,或捕獲,或拋出。那究竟什麼是異常?異常即非正常的,不同於平常、一般化的情況。
在平時生活中,醫生會說你身體的某個部位有異常,該異常會有什麼什麼的影響,是由某某原因引起的;
再比如:我每天都準時打卡,按時上下班,那麼我本月的考勤是正常的,反之,但凡有遲到、曠工、早退的情況之一的,我本月的考勤就會有異常。
而在程序中,代碼在運行中如果出現運行錯誤,程序會終止運行,這時由於錯誤導致程序運行終止的情況就是程序出現了異常。
異常並不是指語法錯誤,因爲如果語法錯了,編譯就通不過,不會產生JVM能夠識別的字節碼文件,是沒法運行起來的,所以只有運行中的程序纔會有異常一說。
Java 異常體系
異常處理是衡量一門語言是否成熟的標準之一,C
系列的語言諸如:Java
、C++
、C
等都支持異常處理,有自己的一套異常處理機制。異常處理機制可以讓程序有更好的容錯性,使代碼更加健壯。
所以,引入異常處理機制可以解決的問題有:
-
使用方法的返回值來表示異常情況有侷限性,需要無窮列舉所有的異常情況;
-
異常流程代碼和正常流程代碼混合一起,代碼往往很臃腫,也很複雜性;代碼的可讀性和可維護性都不高;
-
隨着系統規模的不斷擴大,代碼很難維護,特別是系統可拓展性差;
通過一下這個案例來說明引入異常之前的處理方式:
Car.java:
Officer.java:
WorkDemo.java:
上述案例中,只是簡單粗暴地把汽車的狀態分爲true
和false
兩種,細化不夠,不能體現出汽車的詳細狀態;業務邏輯也很侷限,如果要拓展,就要花費更大的成本。
針對於上述的問題,Java
基於面向對象的思想提出瞭解決方案:
-
把不同類型的異常情況使用不同的類來表示,不同的異常類有共同的父類;
-
分離異常流程代碼和正確流程代碼;
-
規範異常處理機制,靈活處理異常,能處理就將其捕獲並處理,如果處理不了異常,就將其交給調用者來處理;
Java 異常體系:
Java API文檔中的詳細介紹如下:
Error
:表示錯誤,一般指JVM
相關的不可修復的錯誤,如:系統崩潰、內存溢出、JVM
內部錯誤等,由JVM
拋出,一般情況下不需要處理,幾乎其所有的子類都是以“Error
”作爲類名後綴;比如:StackOverflowError
,當應用程序遞歸太深而發生內存溢出時,就會拋出該錯誤。
Exception
:表示異常,指程序中出現不正常的情況,異常一般都是需要程序員來處理的(可以捕獲或者拋出);幾乎其所有的子類都是以“Exception
”作爲類名的後綴;
常見的Exception
有:
1.NullPointerException
:空指針異常,一般當對象爲null
的時候,對該對象做操作時會出現該異常;
2.ArrayIndexOutOfBoundsException
: 數組的索引越界,操作數組時使用的索引超出了數組的數據範圍會出現;
3.NumberFormatException
:數字格式化異常,把非數字的數據類型轉換爲數字類型時使用了非法的轉換對象;
這裏只列出了三種,但Java
中的異常類型遠不止這幾種,想要了解更多可以去查閱JDK
文檔。
Throwable:在Java
體系中,Throwable
類是所有錯誤和異常的父類;當出現了沒見過的異常時,可以將異常類的類名拿到Java API
文檔中去查找,通過文章介紹即可獲得異常的詳細信息,以及其在Java
中的繼承、實現體系;
Java 的異常詳解:
以下是一段運行會出異常的Java 代碼:
運行上述代碼案例,運行結果如下:
如果出現異常,會立刻中斷運行中的程序,所以必須處理異常,而處理方式有兩種:
-
throws
:當前方法不處理,而是聲明拋出,由該方法的調用者來處理; -
try-catch
:在當前方法中使用try-catch
的語句塊來處理異常;
捕獲異常 try-catch
出現異常之後,程序會中斷執行,所以異常必須處理;處理的方式有兩種:捕獲和拋出,兩種方式二選一即可。先來運行一個案例來證明:
運行結果如下:
通過查看運行結果,是期望的運行結果,代碼運行成功;那麼接下來對上述案例稍作修改,再來看其運行結果如何:
運行結果如下:
通過查看運行結果,運行結果並不是想要的,代碼中出現了異常,代碼被中斷運行。對比兩次的運行結果,可以得出結論:出現異常之後,程序會中斷執行,異常必須處理。
try-catch 異常捕獲:使用try-catch
捕獲單個異常,語法如下:
注意:try和catch都不能單獨使用,必須連用。所以可以對上述出現異常的代碼使用try-catch來處理:
運行結果如下:
通過查看運行結果,不難發現,使用try-catch
之後,程序遇到異常時不再中斷執行,而是跳過異常代碼及其之後的在try-catch
中的剩餘代碼語句,來到catch
代碼塊中執行定義的異常處理代碼;
在上述案例中是打印出了異常信息、異常對象信息、異常棧追蹤,其中的3
個方法都是Throwable
類的方法:
-
String getMessage()
:獲取異常的詳細描述信息; -
String toString()
:獲取異常的類型、異常描述信息; -
void printStackTrace()
:打印異常的跟蹤棧信息並輸出到控制檯,但不能在System.out.println()
中使用該方法;其中包含了異常的類型、異常的原因、異常出現的位置;在開發和調試階段,該方法都很有用,方便調試和修改;
底層的異常處理
而在Java
底層,當代碼出現異常時,JVM
會先創建對應的異常類型對象,然後根據異常類型在catch
中進行匹配;
若匹配成功,則會把創建好的異常對象賦值給catch
中聲明的異常對象;若匹配失敗,則會向上拋出異常。
使用try-catch
捕獲多個異常,語法如下:
這裏方式其實就是在單個異常捕獲的基礎上添加了多個異常的匹配,使得異常處理更加精細化。
在使用try-catch
時需要注意:
-
一個
catch
語句,只能捕獲一種類型的異常,如果需要捕獲多種異常類型,就得使用多個catch
語句; -
try-catch
中的代碼在只會出現一種類型的異常,只能一個catch
捕獲,不可能同時匹配多個catch
; -
在有多個
catch
語句的代碼中出現異常時,會從上到下依次匹配catch
語句,所以多個catch
語句應該按照從子類到父類的順序依次定義; -
一旦匹配上其中一個
catch
之後,便不會匹配剩餘的catch
,而是會跳出try-catch
,執行之後的代碼;
捕獲多個異常的案例:
運行結果如下:
異常被成功捕獲,再沒法瞎蹦噠了。
拋出異常
拋出異常有兩種方式:
-
throw
:用於方法內部,用於給調用者返回一個異常對象,和return
一樣會結束當前方法; -
throws
:運用於方法聲明之上,定義於方法參數之後,表示當前方法不處理異常,而是提醒該方法的調用者來處理拋出的異常(一個或者多個異常);如:private static int divide(int num1, int num2) throws Exception {}
throw
語句:運用於方法內部,拋出一個具體的異常對象,中止方法的執行,其語法格式如下:throw new 異常類("異常信息");
一般的,當一個方法出現異常的情況,不知道該方法應該返回什麼時,此時就可以返回一個錯誤,在catch
語句塊中使用throw
繼續向上拋出異常。return
是返回一個值,throw
是返回一個錯誤,返回給該方法的調用者。比如:String
類的charAt
方法就是一個很好的例子:
throws
語句:如果每一個方法都放棄處理異常都直接通過throws
聲明拋出,最後異常會拋到main
方法,如果此時main
方法還不處理,會繼續拋出給JVM
,JVM
底層的處理機制就是打印異常的跟蹤棧信息;runtime
異常,默認就是這種處理方式。
方法重寫(Override)中的throws:
一同: 方法的簽名必須相同。
兩小:
1. 子類方法返回類型和父類方法返回類型相同,或是其子類;
2. 子類方法不能聲明拋出新的異常;
一大: 子類方法的訪問權限必須大於等於父類方法的訪問權限。
異常分類
下圖是Java
中的異常分類體系,Java
中所有的異常都從Throwable
繼承而來,主要分兩大類:Error
(錯誤)和異常(Exception
)。
Throwable體系
異常(Exception
)根據其在編譯時期還是運行時期去檢查異常可分爲:checked異常
和runtime異常
runtime異常
:又稱運行時期異常
,此類型的異常在運行時期檢查;在編譯時期,運行異常並不會檢測,就不會出現,只有在運行到相關代碼時纔會出現;RuntimeException
自身及其子類異常都屬於runtime異常
;
checked異常
:又稱編譯時期異常
,此類型的異常在編譯時期就會檢查,而且是必須處理的,如果沒有處理,就會導致編譯失敗;除了runtime異常
之外的其他異常(包括Exception自身)都屬於checked異常
;
自定義異常
Java
中有着不同的定義好的異常類,分別表示着某一種具體的異常情況,在開發中總是有些異常情況是Java SE
庫中沒有定義好的,此時就可以根據自己業務的異常情況來定義異常類;把這樣的異常類稱爲自定義異常類。
自定義異常類的方式:
受檢查的異常:即checked異常
, 自定義一個受檢查的異常類需要繼承於java.lang.Exception
;
運行時異常:即runtime異常
, 自定義一個運行時期檢查的異常類,需要繼承於java.lang.RuntimeException
;一般在開發中,自定義的異常都是運行時異常。
解決開車上班的案例
現在就可以使用自定義異常來解決開車上班的案例中的異常問題:
異常轉譯和異常鏈
異常轉譯:位於最外層的業務系統不需要關心底層的異常細節,通過捕獲原始的異常,將其轉換爲一個新的不同類型的異常,然後再向上拋出;這個過程稱爲異常轉譯。
在上述例子中:我的車壞了,在catch
中重新拋出一個新的異常(OfficerException
)給我的調用者(老闆),不能把車的異常信息拋給老闆看,因爲老闆不關心這些細節,關心的是我是否遲到。
異常鏈: 把原始異常包裝爲新的異常類,形成多個異常的有序排列;異常鏈由於更加清楚、準確的定位異常出現的位置;在下述案例中,異常一層層拋出,直至異常被處理,在這個過程中,異常鏈就產生了:
Java7的異常新特性
1.增強的throw
: 對比Java 6
和 Java 7
中對於拋出異常的改進來體現。
2.多異常捕獲: 重寫捕獲多個異常案例來體現。
3.自動資源關閉: 資源類必須直接或者間接實現java.lang.AutoCloseable
接口
finally代碼塊
finally
語句塊表示無論如何(也包括髮生異常時)都會最終執行的代碼塊,比如:當在try
語句塊中打開了一些物理資源(磁盤文件/網絡連接/數據庫連接等)時,在使用完之後,都得最終關閉打開的資源。
finally的兩種用法:
1.try...finally
: 此時沒有catch
來捕獲異常,因爲此時根據應用場景會拋出異常,程序員自己不處理;
2.try...catch....finally
: 程序員自己需要處理異常,最終得手動關閉資源。
需要注意的是:finally
不能單獨使用。
finally
不執行的情況:
當只有在try
或者catch
中調用退出JVM
的相關方法,此時finally
纔不會執行,否則finally
修飾的代碼塊永遠會執行。比如:System.exit(0);//退出JVM
。
finally
和 return
如果finally
和return
語句同時存在,永遠返回finally
中的結果,但是應該避免這種情況的出現。詳情看如下的案例:
如果finally
和return
語句同時存在,永遠返回finally
中的結果
還有另一種情況也很有趣,一起來看看:
爲什麼會出現這種情況呢?首先finally肯定是會被執行的,所以a++
之後a
的值變成了14
,但是finally
中沒有返回值,值爲14
的變量a
並沒有被返回;然後接着執行return a
;這裏的a
的值在方法執行之初就已經確定了,故返回的值是13
。
處理異常的原則:
1. 異常只能用於非正常情況,try-catch
的存在也會影響性能,儘量縮小try-catch
的代碼範圍;
2. 需要爲異常提供說明文檔,可以參考Java doc
,如果自定義了異常或某一個方法拋出了異常,應該在文檔註釋中詳細說明;
3. 儘可能避免異常的出現,如NullPointerException
等;
4. 異常的粒度很重要,應該爲一個基本操作定義一個try-catch
塊,切忌將幾百行代碼放到一個 try-catch
塊中;
最後,不要在循環中進行異常處理,應該在循環外對異常進行捕獲處理(在循環之外使用try-catch
);自定義異常儘量使用RuntimeException
類型的,並且要儘量避開已存在的異常;
小結
1. 五個關鍵字:try、catch、finally、throw、throws
;
2. 異常體系的兩個繼承結構:
Throwable 類有兩個子類:Error和Exception。
Exception 類有一個特殊的子類:RuntimeException。
RuntimeException 類及其子類稱之爲runtime異常。
Exception 類和子類中除了RuntimeException體系的其他類稱之爲checked異常。
3. 自定義異常類
4. Error
和Exception
的區別和關係
5. checked異常
和runtime異常
的區別
6. finally
關鍵字及其相關知識
7. finally和return的執行順序
8. throw
和throws
的區別
完結,老夫雖不正經,但老夫一身的才華!關注我,獲取更多編程科技知識。