簡介
程序運行時盒揉,發(fā)生的不被期望的事件钮糖,它阻止了程序按照程序員的預期正常執(zhí)行,這就是異常契邀。異常發(fā)生時偶房,是任程序自生自滅趁曼,立刻退出終止,還是輸出錯誤給用戶棕洋?或者用C語言風格:用函數(shù)返回值作為執(zhí)行狀態(tài)挡闰?。
Java提供了更加優(yōu)秀的解決辦法:異常處理機制掰盘。
異常處理機制能讓程序在異常發(fā)生時摄悯,按照代碼的預先設定的異常處理邏輯,針對性地處理異常愧捕,讓程序盡最大可能恢復正常并繼續(xù)執(zhí)行奢驯,且保持代碼的清晰。
Java中的異炒位妫可以是函數(shù)中的語句執(zhí)行時引發(fā)的瘪阁,也可以是程序員通過throw 語句手動拋出的,只要在Java程序中產生了異常邮偎,就會用一個對應類型的異常對象來封裝異常管跺,JRE就會試圖尋找異常處理程序來處理異常。
Throwable類是Java異常類型的頂層父類禾进,一個對象只有是 Throwable 類的(直接或者間接)實例豁跑,他才是一個異常對象,才能被異常處理機制識別命迈。JDK中內建了一些常用的異常類贩绕,我們也可以自定義異常。
Java異常的分類和類結構圖
Java標準庫內建了一些通用的異常壶愤,這些類以Throwable為頂層父類淑倾。
Throwable又派生出Error類和Exception類。
錯誤:Error類以及他的子類的實例征椒,代表了JVM本身的錯誤娇哆。錯誤不能被程序員通過代碼處理,Error很少出現(xiàn)。因此碍讨,程序員應該關注Exception為父類的分支下的各種異常類治力。
異常:Exception以及他的子類,代表程序運行時發(fā)送的各種不期望發(fā)生的事件勃黍∠常可以被Java異常處理機制使用,是異常處理的核心覆获。
總體上我們根據(jù)Javac對異常的處理要求马澈,將異常類分為2類。
非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類弄息。javac在編譯時痊班,不會提示和發(fā)現(xiàn)這樣的異常,不要求在程序處理這些異常摹量。所以如果愿意涤伐,我們可以編寫代碼處理(使用try…catch…finally)這樣的異常,也可以不處理缨称。對于這些異常凝果,我們應該修正代碼,而不是去通過異常處理器處理 具钥。這樣的異常發(fā)生的原因多半是代碼寫的有問題豆村。如除0錯誤ArithmeticException,錯誤的強制類型轉換錯誤ClassCastException骂删,數(shù)組索引越界ArrayIndexOutOfBoundsException掌动,使用了空對象NullPointerException等等。
檢查異常(checked exception):除了Error 和 RuntimeException的其它異常宁玫。javac強制要求程序員為這樣的異常做預備處理工作(使用try…catch…finally或者throws)粗恢。在方法中要么用try-catch語句捕獲它并處理,要么用throws子句聲明拋出它欧瘪,否則編譯不會通過眷射。這樣的異常一般是由程序的運行環(huán)境導致的。因為程序可能被運行在各種未知的環(huán)境下佛掖,而程序員無法干預用戶如何使用他編寫的程序妖碉,于是程序員就應該為這樣的異常時刻準備著。如SQLException , IOException,ClassNotFoundException 等芥被。
需要明確的是:檢查和非檢查是對于javac來說的欧宜,這樣就很好理解和區(qū)分了。
初識異常
下面的代碼會演示2個異常類型:ArithmeticException 和 InputMismatchException拴魄。前者由于整數(shù)除0引發(fā)冗茸,后者是輸入的數(shù)據(jù)不能被轉換為int類型引發(fā)席镀。
package com.example;
import java. util .Scanner ;
public class AllDemo
{
public static void main (String [] args )
{
System . out. println( "----歡迎使用命令行除法計算器----" ) ;
CMDCalculate ();
}
public static void CMDCalculate ()
{
Scanner scan = new Scanner ( System. in );
int num1 = scan .nextInt () ;
int num2 = scan .nextInt () ;
int result = devide (num1 , num2 ) ;
System . out. println( "result:" + result) ;
scan .close () ;
}
public static int devide (int num1, int num2 ){
return num1 / num2 ;
}
}
/*****************************************
----歡迎使用命令行除法計算器----
0
Exception in thread "main" java.lang.ArithmeticException : / by zero
at com.example.AllDemo.devide( AllDemo.java:30 )
at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )
at com.example.AllDemo.main( AllDemo.java:12 )
----歡迎使用命令行除法計算器----
r
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor( Scanner.java:864 )
at java.util.Scanner.next( Scanner.java:1485 )
at java.util.Scanner.nextInt( Scanner.java:2117 )
at java.util.Scanner.nextInt( Scanner.java:2076 )
at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )
at com.example.AllDemo.main( AllDemo.java:12 )
*****************************************/
異常是在執(zhí)行某個函數(shù)時引發(fā)的,而函數(shù)又是層級調用夏漱,形成調用棧的豪诲,因為,只要一個函數(shù)發(fā)生了異常挂绰,那么他的所有的caller都會被異常影響屎篱。當這些被影響的函數(shù)以異常信息輸出時,就形成的了異常追蹤棧葵蒂。
異常最先發(fā)生的地方芳室,叫做異常拋出點。
從上面的例子可以看出刹勃,當devide函數(shù)發(fā)生除0異常時,devide函數(shù)將拋出ArithmeticException異常嚎尤,因此調用他的CMDCalculate函數(shù)也無法正常完成荔仁,因此也發(fā)送異常,而CMDCalculate的caller——main 因為CMDCalculate拋出異常芽死,也發(fā)生了異常乏梁,這樣一直向調用棧的棧底回溯。這種行為叫做異常的冒泡关贵,異常的冒泡是為了在當前發(fā)生異常的函數(shù)或者這個函數(shù)的caller中找到最近的異常處理程序遇骑。由于這個例子中沒有使用任何異常處理機制,因此異常最終由main函數(shù)拋給JRE揖曾,導致程序終止落萎。
上面的代碼不使用異常處理機制,也可以順利編譯炭剪,因為2個異常都是非檢查異常练链。但是下面的例子就必須使用異常處理機制,因為異常是檢查異常奴拦。
代碼中我選擇使用throws聲明異常媒鼓,讓函數(shù)的調用者去處理可能發(fā)生的異常。但是為什么只throws了IOException呢错妖?因為FileNotFoundException是IOException的子類绿鸣,在處理范圍內。
@Test
public void testException() throws IOException
{
//FileInputStream的構造函數(shù)會拋出FileNotFoundException
FileInputStream fileIn = new FileInputStream("E:\\a.txt");
int word;
//read方法會拋出IOException
while((word = fileIn.read())!=-1)
{
System.out.print((char)word);
}
//close方法會拋出IOException
fileIn.clos
}
異常處理的基本語法
在編寫代碼處理異常時暂氯,對于檢查異常潮模,有2種不同的處理方式:使用try…catch…finally語句塊處理它≈昕酰或者再登,在函數(shù)簽名中使用throws 聲明交給函數(shù)調用者caller去解決尔邓。
try…catch…finally語句塊
try{
//try塊中放可能發(fā)生異常的代碼。
//如果執(zhí)行完try且不發(fā)生異常锉矢,則接著去執(zhí)行finally塊和finally后面的代碼(如果有的話)梯嗽。
//如果發(fā)生異常,則嘗試去匹配catch塊沽损。
}catch(SQLException SQLexception){
//每一個catch塊用于捕獲并處理一個特定的異常灯节,或者這異常類型的子類。Java7中可以將多個異常聲明在一個catch中绵估。
//catch后面的括號定義了異常類型和異常參數(shù)炎疆。如果異常與之匹配且是最先匹配到的,則虛擬機將使用這個catch塊來處理異常国裳。
//在catch塊中可以使用這個塊的異常參數(shù)來獲取異常的相關信息形入。異常參數(shù)是這個catch塊中的局部變量,其它塊不能訪問缝左。
//如果當前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主要做一些清理工作腿倚,如流的關閉纯出,數(shù)據(jù)庫連接的關閉等。
}
需要注意的地方
1敷燎、try塊中的局部變量和catch塊中的局部變量(包括異常變量)暂筝,以及finally中的局部變量,他們之間不可共享使用硬贯。
2焕襟、每一個catch塊用于處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的饭豹,只有第一個匹配的catch會得到執(zhí)行鸵赖。匹配時务漩,不僅運行精確匹配,也支持父類匹配它褪,因此饵骨,如果同一個try塊下的多個catch異常類型有父子關系,應該將子類異常放在前面茫打,父類異常放在后面居触,這樣保證每個catch塊都有存在的意義。
3老赤、java中轮洋,異常處理的任務就是將執(zhí)行控制流從異常發(fā)生的地方轉移到能夠處理這種異常的地方去。也就是說:當一個函數(shù)的某條語句發(fā)生異常時抬旺,這條語句的后面的語句不會再執(zhí)行弊予,它失去了焦點。執(zhí)行流跳轉到最近的匹配的異常處理catch代碼塊去執(zhí)行开财,異常被處理完后块促,執(zhí)行流會接著在“處理了這個異常的catch代碼塊”后面接著執(zhí)行。
有的編程語言當異常被處理后床未,控制流會恢復到異常拋出點接著執(zhí)行,這種策略叫做:resumption model of exception handling(恢復式異常處理模式 )
而Java則是讓執(zhí)行流恢復到處理了異常的catch塊后接著執(zhí)行振坚,這種策略叫做:termination model of exception handling(終結式異常處理模式)
public static void main(String[] args){
try {
foo();
}catch(ArithmeticException ae) {
System.out.println("處理異常");
}
}
public static void foo(){
int a = 5/0; //異常拋出點
System.out.println("為什么還不給我漲工資!!!"); //////////////////////不會執(zhí)行
}
throws 函數(shù)聲明
throws聲明:如果一個方法內部的代碼會拋出檢查異常(checked exception)薇搁,而方法自己又沒有完全處理掉,則javac保證你必須在方法的簽名上使用throws關鍵字聲明這些可能拋出的異常渡八,否則編譯不通過啃洋。
throws是另一種處理異常的方式,它不同于try…catch…finally屎鳍,throws僅僅是將函數(shù)中可能出現(xiàn)的異常向調用者聲明宏娄,而自己則不具體處理。
采取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常逮壁,或者說讓調用者處理更好孵坚,調用者需要為可能發(fā)生的異常負責。
public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{
//foo內部可以拋出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 類的異常窥淆,或者他們的子類的異常對象卖宠。
}
finally塊
finally塊不管異常是否發(fā)生,只要對應的try執(zhí)行了忧饭,則它一定也執(zhí)行扛伍。只有一種方法讓finally塊不執(zhí)行:System.exit()。因此finally塊通常用來做資源釋放操作:關閉文件词裤,關閉數(shù)據(jù)庫連接等等刺洒。
良好的編程習慣是:在try塊中打開資源鳖宾,在finally塊中清理釋放這些資源。
需要注意的地方:
1逆航、finally塊沒有處理異常的能力鼎文。處理異常的只能是catch塊。
2纸泡、在同一try…catch…finally塊中 漂问,如果try中拋出異常,且有匹配的catch塊女揭,則先執(zhí)行catch塊蚤假,再執(zhí)行finally塊。如果沒有catch塊匹配吧兔,則先執(zhí)行finally磷仰,然后去外面的調用者中尋找合適的catch塊。
3境蔼、在同一try…catch…finally塊中 灶平,try發(fā)生異常,且匹配的catch塊中處理異常時也拋出異常箍土,那么后面的finally也會執(zhí)行:首先執(zhí)行finally塊逢享,然后去外圍調用者中尋找合適的catch塊。
這是正常的情況吴藻,但是也有特例瞒爬。關于finally有很多惡心,偏沟堡、怪侧但、難的問題,我在本文最后統(tǒng)一介紹了航罗,電梯速達->:finally塊和return
throw 異常拋出語句
throw exceptionObject
程序員也可以通過throw語句手動顯式的拋出一個異常禀横。throw語句的后面必須是一個異常對象。
throw 語句必須寫在函數(shù)中粥血,執(zhí)行throw 語句的地方就是一個異常拋出點柏锄,它和由JRE自動形成的異常拋出點沒有任何差別。
public void save(User user)
{
if(user == null)
throw new IllegalArgumentException("User對象為空");
//......
}
異常的鏈化
在一些大型的复亏,模塊化的軟件開發(fā)中绢彤,一旦一個地方發(fā)生異常,則如骨牌效應一樣蜓耻,將導致一連串的異常茫舶。假設B模塊完成自己的邏輯需要調用A模塊的方法,如果A模塊發(fā)生異常刹淌,則B也將不能完成而發(fā)生異常饶氏,但是B在拋出異常時讥耗,會將A的異常信息掩蓋掉,這將使得異常的根源信息丟失疹启。異常的鏈化可以將多個模塊的異常串聯(lián)起來古程,使得異常信息不會丟失。
異常鏈化:以一個異常對象為參數(shù)構造新的異常對象喊崖。新的異對象將包含先前異常的信息挣磨。這項技術主要是異常類的一個帶Throwable參數(shù)的函數(shù)來實現(xiàn)的。這個當做參數(shù)的異常荤懂,我們叫他根源異常(cause)茁裙。
查看Throwable類源碼,可以發(fā)現(xiàn)里面有一個Throwable字段cause节仿,就是它保存了構造時傳遞的根源異常參數(shù)晤锥。這種設計和鏈表的結點類設計如出一轍,因此形成鏈也是自然的了廊宪。
public class Throwable implements Serializable {
private Throwable cause = this;
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}
public Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
//........
}
下面是一個例子矾瘾,演示了異常的鏈化:從命令行輸入2個int,將他們相加箭启,輸出壕翩。輸入的數(shù)不是int,則導致getInputNumbers異常傅寡,從而導致add函數(shù)異常放妈,則可以在add函數(shù)中拋出
一個鏈化的異常。
public static void main(String[] args)
{
System.out.println("請輸入2個加數(shù)");
int result;
try
{
result = add();
System.out.println("結果:"+result);
} catch (Exception e){
e.printStackTrace();
}
}
//獲取輸入的2個整數(shù)返回
private static List<Integer> getInputNumbers()
{
List<Integer> nums = new ArrayList<>();
Scanner scan = new Scanner(System.in);
try {
int num1 = scan.nextInt();
int num2 = scan.nextInt();
nums.add(new Integer(num1));
nums.add(new Integer(num2));
}catch(InputMismatchException immExp){
throw immExp;
}finally {
scan.close();
}
return nums;
}
//執(zhí)行加法計算
private static int add() throws Exception
{
int result;
try {
List<Integer> nums =getInputNumbers();
result = nums.get(0) + nums.get(1);
}catch(InputMismatchException immExp){
throw new Exception("計算失敗",immExp); /////////////////////////////鏈化:以一個異常對象為參數(shù)構造新的異常對象赏僧。
}
return result;
}
/*
請輸入2個加數(shù)
r 1
java.lang.Exception: 計算失敗
at practise.ExceptionTest.add(ExceptionTest.java:53)
at practise.ExceptionTest.main(ExceptionTest.java:18)
Caused by: java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
at practise.ExceptionTest.add(ExceptionTest.java:48)
... 1 more
*/
自定義異常
如果要自定義異常類,則擴展Exception類即可扭倾,因此這樣的自定義異常都屬于檢查異常(checked exception)淀零。如果要自定義非檢查異常,則擴展自RuntimeException膛壹。
按照國際慣例驾中,自定義的異常應該總是包含如下的構造函數(shù):
- 一個無參構造函數(shù)
- 一個帶有String參數(shù)的構造函數(shù),并傳遞給父類的構造函數(shù)模聋。
- 一個帶有String參數(shù)和Throwable參數(shù)肩民,并都傳遞給父類構造函數(shù)
- 一個帶有Throwable 參數(shù)的構造函數(shù),并傳遞給父類的構造函數(shù)链方。
下面是IOException類的完整源代碼持痰,可以借鑒。
public class IOException extends Exception
{
static final long serialVersionUID = 7818375828146090155L;
public IOException()
{
super();
}
public IOException(String message)
{
super(message);
}
public IOException(String message, Throwable cause)
{
super(message, cause);
}
public IOException(Throwable cause)
{
super(cause);
}
}
異常的注意事項
1祟蚀、當子類重寫父類的帶有 throws聲明的函數(shù)時工窍,其throws聲明的異常必須在父類異常的可控范圍內——用于處理父類的throws方法的異常處理器割卖,必須也適用于子類的這個帶throws方法 。這是為了支持多態(tài)患雏。
例如鹏溯,父類方法throws 的是2個異常,子類就不能throws 3個及以上的異常淹仑。父類throws IOException丙挽,子類就必須throws IOException或者IOException的子類。
至于為什么匀借?我想颜阐,也許下面的例子可以說明。
class Father
{
public void start() throws IOException
{
throw new IOException();
}
}
class Son extends Father
{
public void start() throws Exception
{
throw new SQLException();
}
}
/**********************假設上面的代碼是允許的(實質是錯誤的)***********************/
class Test
{
public static void main(String[] args)
{
Father[] objs = new Father[2];
objs[0] = new Father();
objs[1] = new Son();
for(Father obj:objs)
{
//因為Son類拋出的實質是SQLException怀吻,而IOException無法處理它瞬浓。
//那么這里的try。蓬坡。catch就不能處理Son中的異常猿棉。
//多態(tài)就不能實現(xiàn)了。
try {
obj.start();
}catch(IOException)
{
//處理IOException
}
}
}
}
2屑咳、Java程序可以是多線程的萨赁。每一個線程都是一個獨立的執(zhí)行流,獨立的函數(shù)調用棧兆龙。如果程序只有一個線程杖爽,那么沒有被任何代碼處理的異常 會導致程序終止。如果是多線程的紫皇,那么沒有被任何代碼處理的異常僅僅會導致異常所在的線程結束慰安。
也就是說,Java中的異常是線程獨立的聪铺,線程的問題應該由線程自己來解決化焕,而不要委托到外部,也不會直接影響到其它線程的執(zhí)行铃剔。
finally塊和return
首先一個不容易理解的事實:在 try塊中即便有return撒桨,break,continue等改變執(zhí)行流的語句键兜,finally也會執(zhí)行凤类。
public static void main(String[] args)
{
int re = bar();
System.out.println(re);
}
private static int bar()
{
try{
return 5;
} finally{
System.out.println("finally");
}
}
/*輸出:
finally
*/
很多人面對這個問題時,總是在歸納執(zhí)行的順序和規(guī)律普气,不過我覺得還是很難理解谜疤。我自己總結了一個方法。用如下GIF圖說明。
也就是說:try…catch…finally中的return 只要能執(zhí)行茎截,就都執(zhí)行了苇侵,他們共同向同一個內存地址(假設地址是0×80)寫入返回值,后執(zhí)行的將覆蓋先執(zhí)行的數(shù)據(jù)企锌,而真正被調用者取的返回值就是最后一次寫入的榆浓。那么,按照這個思想撕攒,下面的這個例子也就不難理解了陡鹃。
finally中的return 會覆蓋 try 或者catch中的返回值。
public static void main(String[] args)
{
int result;
result = foo();
System.out.println(result); /////////2
result = bar();
System.out.println(result); /////////2
}
@SuppressWarnings("finally")
public static int foo()
{
trz{
int a = 5 / 0;
} catch (Exception e){
return 1;
} finally{
return 2;
}
}
@SuppressWarnings("finally")
public static int bar()
{
try {
return 1;
}finally {
return 2;
}
}
finally中的return會抑制(消滅)前面try或者catch塊中的異常
class TestException
{
public static void main(String[] args)
{
int result;
try{
result = foo();
System.out.println(result); //輸出100
} catch (Exception e){
System.out.println(e.getMessage()); //沒有捕獲到異常
}
try{
result = bar();
System.out.println(result); //輸出100
} catch (Exception e){
System.out.println(e.getMessage()); //沒有捕獲到異常
}
}
//catch中的異常被抑制
@SuppressWarnings("finally")
public static int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("我將被忽略抖坪,因為下面的finally中使用了return");
}finally {
return 100;
}
}
//try中的異常被抑制
@SuppressWarnings("finally")
public static int bar() throws Exception
{
try {
int a = 5/0;
return 1;
}finally {
return 100;
}
}
}
finally中的異常會覆蓋(消滅)前面try或者catch中的異常
class TestException
{
public static void main(String[] args)
{
int result;
try{
result = foo();
} catch (Exception e){
System.out.println(e.getMessage()); //輸出:我是finaly中的Exception
}
try{
result = bar();
} catch (Exception e){
System.out.println(e.getMessage()); //輸出:我是finaly中的Exception
}
}
//catch中的異常被抑制
@SuppressWarnings("finally")
public static int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("我將被忽略萍鲸,因為下面的finally中拋出了新的異常");
}finally {
throw new Exception("我是finaly中的Exception");
}
}
//try中的異常被抑制
@SuppressWarnings("finally")
public static int bar() throws Exception
{
try {
int a = 5/0;
return 1;
}finally {
throw new Exception("我是finaly中的Exception");
}
}
}
上面的3個例子都異于常人的編碼思維,因此我建議:
- 不要在fianlly中使用return擦俐。
- 不要在finally中拋出異常脊阴。
- 減輕finally的任務,不要在finally中做一些其它的事情蚯瞧,finally塊僅僅用來釋放資源是最合適的嘿期。
- 將盡量將所有的return寫在函數(shù)的最后面,而不是try … catch … finally中埋合。
參考:
http://www.cnblogs.com/lulipro/p/7504267.html
如果感興趣备徐,請關注我的微信公眾號: