概念介紹
異常是發生在程序執行過程中阻礙程序正常執行的錯誤事件,當一個程序出現錯誤時,可能的情況有如下3種:
- 語法錯誤
代碼的格式錯了,某個字母輸錯了 -
運行時錯誤
空指針異常,數組越界,除數為零等 - 邏輯錯誤
運行結果與預想的結果不一樣,這是一種很難調試的錯誤
Java中的異常處理機制主要處理運行時錯誤。
異常分類
下圖是一張經典的Java異常類層次結構圖,對各種異常做出了較為清晰的分類
從上圖中可以看到,所有的異常都繼承自一個共同的父類Throwable,而Throwable有兩個重要的子類:Exception(異常)和Error(錯誤)
下面對這兩個重要的子類進行介紹
Error(錯誤)
是程序無法處理的錯誤,表示運行應用程序中較嚴重問題。大多數錯誤與代碼編寫者執行的操作無關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。例如,Java虛擬機運行錯誤(Virtual MachineError),當 JVM 不再有繼續執行操作所需的內存資源時,將出現 OutOfMemoryError。這些異常發生時,Java虛擬機(JVM)一般會選擇線程終止。
這些錯誤表示故障發生于虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因為它們在應用程序的控制和處理能力之 外,而且絕大多數是程序運行時不允許出現的狀況。對于設計合理的應用程序來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java中,錯誤通過Error的子類描述。Exception(異常)
是程序本身可以處理的異常。主要包含RuntimeException等運行時異常和IOException,SQLException等非運行時異常。
運行時異常包括:都是RuntimeException類及其子類異常,如NullPointerException(空指針異常)、IndexOutOfBoundsException(下標越界異常)等,這些異常是不檢查異常,程序中可以選擇捕獲處理,也可以不處理。這些異常一般是由程序邏輯錯誤引起的,程序應該從邏輯角度盡可能避免這類異常的發生。
運行時異常的特點是Java編譯器不會檢查它,也就是說,當程序中可能出現這類異常,即使沒有用try-catch語句捕獲它,也沒有用throws子句聲明拋出它,也會編譯通過。
非運行時異常(編譯異常)包括:RuntimeException以外的異常,類型上都屬于Exception類及其子類。從程序語法角度講是必須進行處理的異常,如果不處理,程序就不能編譯通過。如IOException、SQLException等以及用戶自定義的Exception異常,一般情況下不自定義檢查異常。
從編譯器是否要求強制處理的角度分類,異常類別又可分為:
可查異常
正確的程序在運行中,很容易出現的、情理可容的異常狀況。可查異常雖然是異常狀況,但在一定程度上它的發生是可以預計的,而且一旦發生這種異常狀況,就必須采取某種方式進行處理。
除了RuntimeException及其子類以外,其他的Exception類及其子類都屬于可查異常。這種異常的特點是Java編譯器會檢查它,也就是說,當程序中可能出現這類異常,要么用try-catch語句捕獲它,要么用throws子句聲明拋出它,否則編譯不會通過。不可查異常
包括運行時異常(RuntimeException與其子類)和錯誤(Error)。
異常處理機制
在 Java 應用程序中,異常處理機制為:拋出異常,捕捉異常。
拋出異常
當一個方法出現錯誤引發異常時,方法創建異常對象并交付運行時系統,異常對象中包含了異常類型和異常出現時的程序狀態等異常信息。運行時系統負責尋找處置異常的代碼并執行。
注意:對于運行時異常、錯誤或可查異常,Java技術所要求的異常處理方式有所不同。
由于運行時異常的不可查性,為了更合理、更容易地實現應用程序,Java規定,運行時異常將由Java運行時系統自動拋出,允許應用程序忽略運行時異常。
對于方法運行中可能出現的Error,當運行方法不欲捕捉時,Java允許該方法不做任何拋出聲明。因為,大多數Error異常屬于永遠不能被允許發生的狀況,也屬于合理的應用程序不該捕捉的異常。
對于所有的可查異常,Java規定:一個方法必須捕捉,或者聲明拋出方法之外。也就是說,當一個方法選擇不捕捉可查異常時,它必須聲明將拋出異常。捕獲異常
在方法拋出異常之后,運行時系統將轉為尋找合適的異常處理器(exception handler)。潛在的異常處理器是異常發生時依次存留在調用棧中的方法的集合。當異常處理器所能處理的異常類型與方法拋出的異常類型相符時,即為合適 的異常處理器。運行時系統從發生異常的方法開始,依次回查調用棧中的方法,直至找到含有合適異常處理器的方法并執行。當運行時系統遍歷調用棧而未找到合適 的異常處理器,則運行時系統終止。同時,意味著Java程序的終止。
通常使用關鍵字try、catch、finally來捕獲異常
語法形式如下:
try {
// 可能會發生異常的程序代碼
} catch (Type1 id1) {
// 捕獲并處理try拋出的異常類型Type1
} catch (Type2 id2) {
// 捕獲并處理try拋出的異常類型Type2
} finally {
// 無論是否發生異常,都將執行的語句塊
}
小結:
try 塊:用于捕獲異常。其后可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。
catch 塊:用于處理try捕獲到的異常。
finally 塊:無論是否捕獲或處理異常,finally塊里的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。在以下4種特殊情況下,finally塊不會被執行:
1)在finally語句塊中發生了異常。
2)在前面的代碼中用了System.exit()退出程序。
3)程序所在的線程死亡。
4)關閉CPU。
try、catch、finally語句塊的執行順序:
1)當try沒有捕獲到異常時:try語句塊中的語句逐一被執行,程序將跳過catch語句塊,執行finally語句塊和其后的語句;
2)當try捕獲到異常,catch語句塊里沒有處理此異常的情況:當try語句塊里的某條語句出現異常時,而沒有處理此異常的catch語句塊時,此異常將會拋給JVM處理,finally語句塊里的語句還是會被執行,但finally語句塊后的語句不會被執行;
3)當try捕獲到異常,catch語句塊里有處理此異常的情況:在try語句塊中是按照順序來執行的,當執行到某一條語句出現異常時,程序將跳到catch語句塊,并與catch語句塊逐一匹配,找到與之對應的處理程序,其他的catch語句塊將不會被執行,而try語句塊中,出現異常之后的語句也不會被執行,catch語句塊執行完后,執行finally語句塊里的語句,最后執行finally語句塊后的語句;
流程如下圖所示:
面試??紗栴}總結
1.描述Java 7 ARM(Automatic Resource Management,自動資源管理)特征和多個catch塊的使用
如果一個try塊中有多個異常要被捕獲,catch塊中的代碼會變丑陋的同時還要用多余的代碼來記錄異常。有鑒于此,Java 7的一個新特征是:一個catch子句中可以捕獲多個異常。示例代碼如下:
catch(IOException | SQLException | Exception ex){
logger.error(ex);
throw new MyException(ex.getMessage());
}
大多數情況下,當忘記關閉資源或因資源耗盡出現運行時異常時,我們只是用finally子句來關閉資源。這些異常很難調試,我們需要深入到資源使用的每一步來確定是否已關閉。因此,Java 7用try-with-resources進行了改進:在try子句中能創建一個資源對象,當程序的執行完try-catch之后,運行環境自動關閉資源。下面是這方面改進的示例代碼:
try (MyResource mr = new MyResource()) {
System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
e.printStackTrace();
}
2.在Java中throw與throws關鍵字之間的區別?
throws用于在方法簽名中聲明此方法可能拋出的異常,而throw關鍵字則是中斷程序的執行并移交異常對象到運行時進行處理。
3.被檢查的異常和不受檢查的異常有什么區別?
被檢查的異常應該用try-catch塊代碼處理,或者在main方法中用throws關鍵字讓JRE了解程序可能拋出哪些異常。不受檢查的異常在程序中不要求被處理或用throws語句告知。
Exception是所有被檢查異常的基類,然而,RuntimeException是所有不受檢查異常的基類。
被檢查的異常適用于那些不是因程序引起的錯誤情況,比如:讀取文件時文件不存在引發的FileNotFoundException。然而,不被檢查的異常通常都是由于糟糕的編程引起的,比如:在對象引用時沒有確保對象非空而引起的NullPointerException。
4.Java中final,finally,finalize的區別?
final和finally在Java中是關鍵字,而finalize則是一個方法。
- final關鍵字使得類變量不可變,避免類被其它類繼承或方法被重寫。
- finally跟try-catch塊一起使用,即使是出現了異常,其子句總會被執行,通常,finally子句用來關閉相關資源。
- finalize方法中的對象被銷毀之前會被垃圾回收。
5.下面是一些代碼相關的問題,需要回答該代碼有沒有問題?該怎么修改?
A.下面這段代碼有什么問題呢?
import java.io.FileNotFoundException;
import java.io.IOException;
public class TestException {
public static void main(String[] args) {
try {
testExceptions();
} catch (FileNotFoundException | IOException e) {
e.printStackTrace();
}
}
public static void testExceptions() throws IOException,
FileNotFoundException {
}
}
上面代碼的主要問題在于FileNotFoundException是IOException的子類,編譯會報錯:The exception FileNotFoundException is already caught by the alternative IOException.
有兩種辦法可以解決這個問題
- 用兩個catch子句來處理這兩個異常
try {
testExceptions();
}catch(FileNotFoundException e){
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
- 在catch子句中移除FileNotFoundException,只用IOException
try {
testExceptions();
}catch (IOException e) {
e.printStackTrace();
}
B.下面這段代碼又有什么問題呢?
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.xml.bind.JAXBException;
public class TestException1 {
public static void main(String[] args) {
try {
go();
} catch (IOException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (JAXBException e) {
e.printStackTrace();
}
}
public static void go() throws IOException, JAXBException, FileNotFoundException{
}
}
上面代碼的問題同樣在于FileNotFoundException是IOException的子類,所以,FileNotFoundException的catch子句將被隱藏。編譯時會報錯:Unreachable catch block for FileNotFoundException.
解決方案:
改變catch子句的順序來修復程序
try {
go();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JAXBException e) {
e.printStackTrace();
}
C.下面的代碼同樣存在問題。
import java.io.IOException;
import javax.xml.bind.JAXBException;
public class TestException2 {
public static void main(String[] args) {
try {
foo();
} catch (IOException e) {
e.printStackTrace();
}catch(JAXBException e){
e.printStackTrace();
}catch(NullPointerException e){
e.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}
}
public static void foo() throws IOException{
}
}
這段代碼同樣不能編譯,因為JAXBException是個受檢查的異常,而foo方法應該拋出此異常供調用方法捕獲。你將會得到:Unreachable catch block for JAXBException這樣的錯誤信息。這個異常不可能從try子句中拋出。為解決這個錯誤,只能將JAXBException從catch子句中移除。
也要注意到,NullPointerException的異常捕獲是有效的,因為它是個不被檢查的異常。
D.下面的代碼存在什么問題呢?
public class TestException3 {
public static void main(String[] args) {
try{
bar();
}catch(NullPointerException e){
e.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}
foo();
}
public static void bar(){
}
public static void foo() throws NullPointerException{
}
}
這代碼是個幌子,根本沒問題,能被正確編譯。我們能捕獲到一般異?;蛘呤遣槐粰z查的異常,即使在throws語句中沒被提及。
同樣,如果程序中的一個方法foo()在throws中聲明了不被檢查的異常,程序中也不一定要處理這個異常。
E.下面這段代碼同樣存在瑕疵。
import java.io.IOException;
public class TestException4 {
public void start() throws IOException{
}
public void foo() throws NullPointerException{ }
}
class TestException5 extends TestException4{
public void start() throws Exception{ }
public void foo() throws RuntimeException{
}
}
這段代碼不能被編譯,因為父類中start的方法簽名與子類中的start方法簽名不相同。為糾正這錯誤,我們可以修改子類的方法簽名使之與超類相同,我們也可以像下面代碼那樣移除子類中throws關鍵字。
@Override
public void start(){
}
F.下面的代碼存在什么問題呢?
import java.io.IOException;
import javax.xml.bind.JAXBException;
public class TestException6 {
public static void main(String[] args) {
try {
foo();
} catch (IOException | JAXBException e) {
e = new Exception("");
e.printStackTrace();
}catch(Exception e){
e = new Exception("");
e.printStackTrace();
}
}
public static void foo() throws IOException, JAXBException{
}
}
這段代碼同樣不能編譯,因為在多個catch子句中的異常對象是不可變的,我們不能改變其值。你會得到這樣的:The parameter e of a multi-catch block cannot be assigned編譯時錯誤信息。我們需要刪掉將e賦值給新異常對象這句來修正錯誤。
參考資料
[1]詳解Java中異常處理機制
[2]深入理解java異常處理機制
[3]JAVA異常處理相關面試題
[4]Java異常的面試問題及答案-Part 1
[5]Java異常的面試問題及答案-Part 2
[6]Java異常的面試問題及答案-Part 3