因爲java是c\c++ 發展而來的, 首先咱們先看看c語言裏的錯誤.java
咱們實現1個程序的過程包括, 代碼編寫, 編譯代碼成爲程序, 執行程序.c++
其中大部分常見的語法錯誤都會被編譯代碼這樣部過濾掉. 可是即便經過了編譯. 執行程序這一步可能仍是會有錯誤.程序員
緣由不少, 例如常見的除數爲0, 內存溢出(數組的index超出界限), 或者內存被其餘程序修改等.面試
最簡單的例子:編程
[java] view plain copy數組
上面的例子編譯時是無錯的, 可是一旦執行就會提示吐核錯誤了. 安全
c語言裏對這種執行時出現的錯誤是無能爲力的, 一旦出錯就會整個程序崩潰, 就不會在繼續執行下面的代碼.jvm
並且不少時候出錯信息不多, 讓你沒法判斷出錯的緣由和地方, 只能一步步當心debug...函數
因此不少用c寫的程序有時會出現非法關閉的現象.spa
解決方法只能是在代碼裏對可能出錯的地方添加if 判斷.
例如f()函數裏能夠對b進行判斷, 若是是0就不執行.
java裏編譯器對代碼的規範性比c嚴格得多. 可是即便如此, 經過編譯的java程序有時也很難避免執行時出錯.
例如, 將上面的c程序改編成java程序:
[java] view plain copy
運行時同樣會出錯, 下面是出錯信息:
[java] view plain copy
可是能夠見到, java告訴你出錯的類型: 運算錯誤(ArithmeticExcetion), 出錯信息和出錯的類與文件行數輸出, 方便你調試. jvm虛擬機是會對錯誤做出必定的處理的.
因此能夠簡單地將java裏的異常理解成java運行時出現的錯誤, 異常機制就是對這種錯誤進行處理的機制.
實際上, 當java程序執行時出現錯誤時, jvm會把執行時出錯的信息(例如出錯緣由, 類型, 位置) 收集,而後打包成爲1個對象(object), 程序員能夠對這種對象進行處理. 這種對象就是所謂的異常.
可能出現的異常的代碼並非確定會出現異常, 取決於執行環境和數據.!
見下圖:
Throwable
/ \
Error Exception
/ / \
xxxxxx xxxxxx RuntimeException
/ \
xxxxxx ArithmeticException
上圖的全部對象都是類.
Throwable 表明是可拋出的.
Error 表明的是嚴重錯誤, 這種錯誤程序員沒法進行處理, 例如操做系統崩潰, jvm出錯, 動態連接庫失敗等. Error並非異常, 不是本文的重點.
Exception 表明的就是異常了. 它下面不少派生類, 其中它的派生類也分兩種, 一種是RuntimeException(運行時異常), 其餘的都是非運行時異常
RuntimeException 包括除數爲0, 數組下標超界等. 運行時異常的派生類有不少, 其產生頻率較高. 它的派生類能夠由程序處理或者拋給(throw) 給jvm處理. 例如上面的例子就是拋給了jvm處理, jvm把程序中斷執行, 並把錯誤信息輸出到終端上.
非RuntimeExcption 這種異常屬於Excepion的派生類(上面紅色的xxx), 可是不是RuntimeException的派生類, 這種異常必須由程序員手動處理,不然不經過編譯.
ArithmeticExcpetion 算術異常, 它是RuntimeException的派生類, 因此程序員不手動處理也經過編譯, 只不過出錯時會被jvm處理.
java裏對異常的處理有三種.
例如咱們將上面的例子改動一下:
[java] view plain copy
在f()函數中對可能出現的異常的代碼進行try catch處理後, 程序會執行catch裏的代碼. 並且不會中斷整個程序, 繼續執行try catch後面的代碼.
程序執行輸出:
[java] view plain copy
注意最終也執行了g()函數中的最後一條語句, 輸出了i的值.
也就是說try catch處理後並不會終止程序, 令程序即便出現了錯誤, 也能夠對錯誤進行必定的處理後繼續執行. 這就是java異常機制比c語言安全的地方.
下面會詳細講解 try catch.
注:
getMessage() 方法: Exception類的方法之一, 返回異常的緣由, 上面的 / by zero 就是這個方法輸出的.
printStackTrace(): Exception類的方法之一, 在屏幕輸出函數棧信息, 也就是異常出現的地方.
例如我在f()函數中不想處理可能出現的異常, 想把它拋出上級函數處理:
下面是個例子:
[java] view plain copy
能夠見到f() 加了個條件判斷, 若是參數b = 0, 使用throw 直接手動拋出1個異常. 讓調用它的函數處理.
g()調用f()函數, 預見到f()可能有異常, 可是也不想處理, 使用throws 關鍵字告訴調用它的函數本函數有可能拋出這種異常. // 注, 這裏的throws對程序並無實質的影響.
h()調用g(), 簡單g()定義的throws, 用try catch在本函數進行處理.
輸出:
[java] view plain copy
注意這個程序沒有執行g() 最後的代碼.
throw 和 throws 後面也會詳細講解.
假如上面的例子h() 也不處理怎麼辦? 就如1.2 的例子, 會拋給jvm處理.
可是這種狀況只適用於RuntimeExecption及其派生類.
jvm怎麼處理呢, 就是中斷整個程序, 並把異常信息輸出到屏幕上.
實際上, 當java程序的1個函數拋出異常時,
首先會檢查當前函數有沒有try catch處理, 若是無檢查上一級函數有無try..catch處理....
這樣在函數棧裏一級一級向上檢查, 若是直至main函數都無try..catch, 則拋給jvm..
項目中強烈建議儘可能手動處理, 不要把異常交給jvm.
這裏開始詳解try catch finally了.
語法是這樣的.
try{
可能出異常的若干行代碼;
}
catch(ExceptionName1 e){
產生ExceptionName 1的處理代碼;
}
catch(ExceptionName2 e){
產生ExceptionName 2的處理代碼;
}
...
finally{
不管如何, 最終確定會執行的代碼
}
下面用個例子來講明:
[java] view plain copy
當f()拋出了異常, 那麼ff()就不會執行了. 程序會嘗試捕捉異常.
首先捕捉ArithmeticException, 捕捉失敗.
接下來捕捉IOException, 捕捉成功, 執行gg();
一旦捕捉到一個異常, 不會再嘗試捕捉其餘異常, 直接執行finally裏的h();
執行後面的函數k().
也就是說路線是:
f() -> gg() -> h() -> k()
有2點要注意的.
1. f()函數極有可能未完整執行, 由於它拋出了異常, 拋出異常的語句執行失敗, 以後的語句放棄執行.
2. try{} 裏面, f()以後的語句, 例如ff()放棄執行.
這種狀況很簡單, 就是try{}裏面的代碼被完整執行, 由於沒有拋出任何異常, 就不會嘗試執行catch裏的部分, 直接到finally部分了.
路線是:
f() -> ff() -> h() -> k()
也許有人會問, 咱們怎麼知道到底會拋出什麼異常?
下面有3個解決方案.
1.看代碼憑經驗, 例如看到1段除法的代碼, 則有可能拋出算術異常.
2.在catch的括號裏寫上Exception e, 畢竟Exception 是全部其餘異常的超類, 這裏涉及多態的知識, 至於什麼是多態能夠看看本人的另外一篇文章.
3. 觀察被調用函數的函數定義, 若是有throws後綴, 則能夠嘗試捕捉throws 後綴拋出的異常
包括我在內不少人會以爲finally語句簡直多勾餘, 既然是否捕捉到異常都會執行, 上面那個例子裏的h()爲何不跟下面的k() 寫在一塊兒呢.
上面的例子的確看不出區別.
但下面兩種狀況下就體現了finally獨特的重要性.
例如try裏面拋出了1個A異常, 可是隻有後面只有捕捉B異常, 和C異常的子句.
這種狀況下, 程序直接執行finally{}裏的子句, 而後中斷當前函數, 把異常拋給上一級函數, 因此當前函數finally後面的語句不會被執行.
例子:
[java] view plain copy
我所說的狀況, 就在上面例子裏的g()函數, g()函數裏嘗試捕捉兩個異常, 可是拋出了第3個異常(ArithmeticException 算術異常).
因此這個異常會中斷g()的執行, 由於沒有被捕捉到, 而後拋給調用g()的 h()函數處理, 而在h()捕捉到了, 因此h()函數是能完整執行的.
也就是說g()裏的
[java] view plain copy
執行失敗
而h()裏的
[java] view plain copy
執行成功
可是不管如何, g()裏的finally{}部分仍是被執行了
執行結果以下:
[java] view plain copy
這種狀況是1中編程的低級錯誤, 在項目中是不容許出現.
避免方法也十分簡單, 在catch子句集的最後增長1個catch(Exception e)就ok, 由於Exception是全部異常的超類, 只要有異常拋出, 則確定會捕捉到.
下面例子:
[java] view plain copy
假如在f()函數拋出了IOExcepion 異常被捕捉到.
那麼執行路線就是
f() -> gg() -> j() -> h() -> 上一級function
也就說, 這種狀況下finally裏的子句會在return回上一級function前執行. 然後面的k()就被放棄了.
能夠看出, finally裏的語句, 不管如何都會被執行.
至有兩種狀況除外, 一是斷電, 二是exit函數.
在項目中, 咱們通常在finally編寫一些釋放資源的動做, 例如初始化公共變量. 關閉connections, 關閉文件等.
這個上面提到過了, 一旦捕捉到1個異常, 就不會嘗試捕捉其餘異常.
若是try裏面的一段代碼可能拋出3種異常A B C,
首先看它先拋出哪一個異常, 若是先拋出A, 若是捕捉到A, 那麼就執行catch(A)裏的代碼. 而後finally.. B和C就沒有機會再拋出了.
若是捕捉不到A, 就執行finally{}裏的語句後中斷當前函數, 拋給上一級函數...(應該避免)
兩種狀況, 1就是沒有異常拋出, 另外一種就是拋出了異常可是沒有捕捉不到(應該避免)
加入try 裏面嘗試捕捉兩個異常, 1個是A, 1個是B, 可是A是B的父類.
這種狀況下, 應該把catch(B)寫在catch(A)前面.
緣由也很簡單, 加入把catch(A)寫在前面, 由於多態的存在, 即便拋出了B異常, 也會被catch(A)捕捉, 後面的catch(B)就沒有意義了.
也就是說若是捕捉Exception這個異常基類, 應該放在最後的catch裏, 項目中也強烈建議這麼作, 能夠避免上述4.3.1的狀況出現.
這個沒什麼好說的. 語法規則
每1個異常對象只能由catch它的catch子句裏訪問.
跟if相似, 很少說了.
這個也不難理解..
下面開始詳講異常另外一種處理方法throw 和 throws了.
注意的是, 這兩種用法都沒有真正的處理異常, 真正處理的異常方法只有try catch, 這兩種方法只是交給上一級方法處理.
就如一個組織裏 , 有1個大佬, 1個黨主, 1個小弟.
大佬叫黨主幹活, 堂主叫小弟幹活, 而後小弟碰上麻煩了, 可是小弟不會處理這個麻煩, 只能中斷工做拋給黨主處理, 而後堂主發現這個麻煩只有大佬能處理, 而後拋給大佬處理..
道理是相通的..
throws的語法很簡單.
語法:
throw new XException();
其中xException必須是Exception的派生類.
這裏注意throw 出的是1個異常對象, 因此new不能省略
做用就是手動令程序拋出1個異常對象.
咱們看回上面3.2 的例子:
[java] view plain copy
當這個函數的if 判斷了b=0時, 就利用throws手動拋出了1個異常. 這個異常會中斷這個函數. 也就是說f()執行不完整, 是沒有返回值的.
[java] view plain copy
例如上沒的g()函數, 在調用f() 會收到1個異常.
這時g()函數有三種選擇.
1. 不作任何處理
這時, g()收到f()裏拋出的異常就會打斷g()執行, 也就是說g()裏面的k(); 被放棄了, 而後程序會繼續把這個函數拋給調用g()函數.
而後一級一級尋求處理, 若是都不處理, 則拋給jvm處理. jvm會中斷程序, 輸出異常信息. 這個上沒提到過了.
2. 使用try catch處理
若是catch成功, 則g()函數能完整執行, 並且這個異常不會繼續向上拋.
若是catch失敗(儘可能避免), 則跟狀況1相同.
將上面的例子改一下:
[java] view plain copy
例如, 我不想拋出ArithmeticException, 我想拋出IOExcetpion.
注意 這裏, IOException雖然邏輯上是錯誤的(徹底不是IO的問題嘛), 可是在程序中徹底可行, 由於程序猿能夠根據須要控制程序指定拋出任何1個異常.
可是這段代碼編譯失敗, 由於IOException 不是 RuntimeException的派生類.
java規定:
改爲這樣就正確了:
[java] view plain copy
注意在方法定義里加上了throws子句. 告訴調用它的函數我可能拋出這個異常.
例如抄回上面的例子, g()調用f()函數.
[java] view plain copy
可是編譯失敗.
由於f()利用throws 聲明瞭會拋出1個非runtimeExcetpion. 這時g()必須作出處理.
處理方法有兩種:
1. try catch本身處理:
[java] view plain copy
須要注意的是, catch裏面要麼寫上throws對應的異常(這裏是 IOException), 要麼寫上這個異常的超類, 不然仍是編譯失敗.
2.g()利用throws 往上一級方法拋
.
[java] view plain copy
這是調用g()的函數也要考慮上面的這兩種處理方法了...
可是最終上級的方法(main 方法)仍是不處理的話, 就編譯失敗, 上面說過了, 非runtimeException沒法拋給jvm處理.
雖然這兩種處理方法都能經過編譯, 可是運行效果是徹底不一樣的.
第一種, g()能完整執行.
第二種, g()被中斷, 也就是g()裏面的k(); 執行失敗.
throws稍微比throw難理解點:
語法是:
public void f() throws Exception1, Exception2...{
}
也就是講, thorws能夠加上多個異常, 注意這裏拋出的不是對象, 不能加上new.
並且不是告訴別人這個函數有可能拋出這麼多個異常. 而是告訴別人, 有可能拋出這些異常的其中一種.
若是爲f()函數加上throws後續, 則告訴調用f()的方法, f()函數有可能拋出這些異常的一種.
若是f()throws 了1個或若干個非RuntimeException, 則調用f()的函數必須處理這些非RuntimeException, 如上面的g()函數同樣.
若是f() throws的都是RuntimeException, 則調用f()的函數能夠不處理, 也能經過編譯, 可是實際上仍是強烈建議處理它們.
實際上, 若是1個方法f() throws A,B
那麼它有可能不拋出任何異常.(程序運行狀態良好)
也有能拋出C異常(應該避免, 最好在throws上加上C)
這個是強制, 告訴別人這個函數內有炸彈.
這個是非強制的, 可是若是你知道一個函數內的代碼有可能拋出異常, 最好仍是寫上throws 後綴
不管這個異常是否runtimeExcepion.
我的建議, 若是你調用1個函數throws A, B, C
那麼你就在當前函數寫上
try
catch(A)
catch(B)
catch(C)
catch(Exception)
這樣能處理能保證你的函數能完整執行, 不會被收到的異常中斷.
固然若是你容許你的函數能夠被中斷, 那麼就能夠在當前函數定義加上throws A, B 繼續拋給上一級的函數.
例如你在一個派生類重寫一個方法f(), 在超類裏的f() throws A, B 你重寫方法時就不throws出 A,,B,C 或者throws A和B的超類.
緣由也是因爲多態的存在.
由於1個超類的引用能夠指向1個派生類的對象並調用不一樣的方法. 若是派生類throws的範圍加大
那麼利用多態寫的代碼的try catch就再也不適用.
面試問得多,單獨拉出來寫了:
應付面試官.
因此throw後面的通常加上new 和exception名字().
而throws後面不能加上new的
由於一旦一個函數throw出1個異常, 這個函數就會被中斷執行, 後面的代碼被放棄, 若是你嘗試在函數內寫兩個throw, 編譯失敗.
而throws 是告訴別人這個函數有可能拋出這幾種異常的一種. 可是最多隻會拋出一種.
緣由上面講過了.
咱們能夠自定義異常, 只須要編寫1個類, 繼承1個異常類就ok
例子:
[java] view plain copy
上面的類User_Exception1 就是1個自定義異常, 並重寫了printStackTrace()方法.
咱們要理解異常的優缺點, 首先看看沒有異常的C語言是如何處理錯誤的.
下面是個例子:
[cpp] view plain copy
能夠見到c語言處理錯誤有這些特色
1. 大部分精力都在錯誤處理.
2. 須要把各類可能出現的錯誤所有考慮到, 才能保證程序的穩定性.
3. 程序可讀性差, 錯誤處理代碼混雜在其餘代碼中.
4. 出錯返回信息少, 一旦出錯難以調試.
5. 一旦出現了未考慮到的錯誤, 資源釋放代碼沒法執行.
[java] view plain copy