什麼是異常
作爲一門面向對象的語言,用Java編寫代碼的過程,可以理解爲創建、使用對象的過程。
普通對象是對象,異常對象也是對象。如果把普通對象比作常人,那麼異常對象就可以理解爲病人。普通對象的作用是爲了讓你的程序運行,而異常對象的作用恰好相反,它的出現就是爲了告訴你程序“生病”了,你必須去“治療”它,否則就無法正常運行。
Java的異常體系
一般來說,異常分爲下面三類:
- 編譯時異常:最具代表的檢查性異常是用戶錯誤或問題引起的異常,這是程序員無法預見的。例如要打開一個不存在文件時,一個異常就發生了,這些異常在編譯時不能被簡單地忽略。
- 運行時異常: 運行時異常是可能被程序員避免的異常。與檢查性異常相反,運行時異常可以在編譯時被忽略。
- 錯誤: 錯誤不是異常,而是脫離程序員控制的問題。錯誤在代碼中通常被忽略。例如,當棧溢出時,一個錯誤就發生了,它們在編譯也檢查不到的。
Exception類繼承於Throwable類。Throwable類有兩個子類,還有一個就是Error類。Error一般是碰到致命錯誤纔會出現,比如服務器宕機。Error不是我們程序員解決的問題,需要我們去解決的是Exception。
Exception可以分爲兩類,RuntimeException(運行時異常)和其餘異常(編譯時異常)。在運行代碼時纔出現的異常,就是運行時異常,運行時異常不會在編譯時報錯。下面通過一個例子演示一下異常:
public class Test {
public static void main(String[] args) {
int num = div(10, 0); // java.lang.ArithmeticException: / by zero
System.out.println(num);
}
public static int div(int a, int b) {
return a / b; // 除數不能爲0
// 除數爲0,創建異常對象new ArithmeticException(" / by zero")
}
}
運行上面的代碼就會出現異常(ArithmeticException),這個異常是怎麼出現的呢?在調用div()方法的時候,我們傳入了除數參數0。很明顯除數是不能爲零的,於是div()方法就不會返回a/b,而是會創建ArithmeticException對象往上拋。而要去接住ArithmeticException對象的是num,num是int類型,自然接不住。num繼續將ArithmeticException對象向上拋給main()方法,main()方法也接不住,main()方法接着把將ArithmeticException對象向上拋給JVM。JVM無法繼續向上拋出異常,將異常信息打印出來。
總結:
- 默認異常處理的方式是層層向上拋
- 異常對象也是對象,只是該對象創建出來,程序就會停止。
異常處理
異常處理一共有兩種方式,一種是前面提到的拋出,另一種是try...catch。
1. throws與throw
throws關鍵字用於方法簽名上,後面接異常類名,throw關鍵字用於方法內,後面跟異常對象。
使用throws / throw拋出異常的類型是RuntimeException及其子類(運行時異常),JVM不會強制你處理拋出的異常,換而言之拋出運行時異常和不拋出異常的效果是一樣的。
反之如果拋出異常的類型是除RuntimeException及其子類之外的異常(編譯時異常),就必須處理拋出的異常。
1.1 運行時異常
public class ThrowException {
public int div(int a, int b) throws ArithmeticException {
return a/b;
}
}
public class Test {
public static void main(String[] args) {
ThrowException te = new ThrowException();
int x = te.div(10, 0);
System.out.println(x);
}
}
上面的例子中,調用div()方法可能出現異常的類型是ArithmeticException(運行時異常)。所以當調用div()方法時,就不是一定要處理div()方法拋出的異常。
1.2 編譯時異常
編譯時異常會在編譯期報錯,也就是說我們必須對編譯時異常進行處理,否則代碼根本無法運行。下面演示編譯時異常的例子:
public class Test {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream fs = new FileInputStream("E:\\test.txt");
}
}
FileInputStream接受一個文件路徑作爲參數,運行代碼時JVM會根據這個路徑找到對應的文件。例子中我給出的參數是“E:\\test.txt”,這個文件是真實存在的,那麼我也可以給這樣一個參數“Z:\\test.txt”,我的電腦中根本不存在Z盤,這樣一來JVM就找不到相應的文件了。所以在例子中,我們必須處理異常(FileNotFoundException),處理的方式是繼續向上拋出。
也就是說相較於運行時異常的“意料之外”,編譯時異常則是“未雨綢繆”。編譯時異常是我們可以預料到的,所以我們必須將這種異常處理掉。
1.3 關鍵字throw
寫到這裏,大家可能覺得出現異常一定都是壞事,其實並不盡然。看這樣一個例子:
public class Person {
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.setName("張三");
p.setAge(-18);
System.out.println(p); // Person [name=張三, age=-18]
}
}
很明顯一個人的年齡不可能是負數,所以當代碼執行到p.setAge(-18)時,程序就不應該再往下繼續執行了。那麼這時候就應該出現異常來中斷程序的執行,下面我們對Person類做出一點修改:
public class Person {
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
if (age > 0 && age < 200) {
this.age = age;
} else {
throw new ArithmeticException("/年齡錯誤");
}
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
再運行上面的代碼就不會出現錯誤的打印結果了,而是會出現異常信息:“java.lang.ArithmeticException: /年齡錯誤”。例子中我們在setAge()方法中創建了一個ArithmeticException異常對象,由於ArithmeticException屬於運行時異常,所以setAge()方法不用再繼續拋出異常。
2. try...catch
然而只是將異常一味的向上拋是不合理的,因爲即使出現了異常,我們的程序也要繼續執行下去,所以大多數時候異常需要我們自己去解決。
解決異常的方法:try...catch...finally
- try : 檢測異常
- catch : 捕獲異常
- finally : 釋放資源
finally這裏先不說,下面我們使用try...catch來解決剛剛碰到的異常:
public class Test {
public static void main(String[] args) {
try {
int num = div(10, 0);
System.out.println(num);
} catch (ArithmeticException e) {
System.out.println("除數爲0");
}
}
public static int div(int a, int b) {
return a / b;
}
}
代碼中我們把可能出現問題的語句放在try代碼塊中,這樣一來ArithmeticException對象一旦被創建出來,就會被catch和捕獲,然後執行catch代碼塊中的語句,打印“除數爲0”。
當然,catch的參數列表中也可以直接使用Exception類型,這樣一來就變成父類引用(Exception)指向子類對象(ArithmeticException )。
有時候代碼中會有多種異常,這時候就可以使用一個try可以和多個catch匹配,看下面一個例子:
public class Test {
public static void main(String[] args) {
int[] arr = {11,22,33};
int a = 10;
int b = 0;
try {
System.out.println(a/b);
System.out.println(arr[4]);
} catch (ArithmeticException e) {
System.out.println("除數爲0");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引越界");
}
System.out.println("finish");
}
}
運行上面的代碼,可以看到打印結果是“除數爲0”和“finish”。也就是說,同時出現多個異常只會捕獲第一個出現的異常,而且異常被捕獲之後,代碼會繼續向下執行。
那麼如果上面的代碼中有三種異常呢?這裏言外之意是,我們有時候會沒法把問題考慮的很全面。這裏可以這樣來解決:
public class Test {
public static void main(String[] args) {
int[] arr = {11,22,33};
int a = 10;
int b = 0;
try {
arr = null;
System.out.println(arr[4]);
} catch (ArithmeticException e) {
System.out.println("除數爲0");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引越界");
} catch (Exception e) {
System.out.println("出現問題");
}
}
}
把最後一個catch的參數設置爲Exception類型,這樣不管出現什麼異常都可以接住。
既然這樣的話,只使用一個catch(Exception e)就可以捕獲所有的異常,爲什麼還要費勁的寫這麼多的catch用於捕獲各式各樣的異常呢?
如果我們只使用一個catch(Exception e)來捕獲所有的異常,那麼當我們看到異常信息,就無法第一時間判斷出具體出現了什麼問題,因爲所有的異常信息都是“出現問題”,最後還是要通過重新檢查代碼來確定具體出現了什麼問題。所以一開始就用catch捕獲的可以明確類型的異常,可以省去後面很多沒必要浪費的時間。
2.1異常信息
當catch捕獲到異常對象時,我們可以用這個異常對象來做什麼呢?看下面一個例子:
public class Test {
public static void main(String[] args) {
try {
System.out.println(1/0);
} catch (ArithmeticException e) {
System.out.println(e.getMessage()); // / by zero
System.out.println(e.toString()); // java.lang.ArithmeticException: / by zero
e.printStackTrace(); /* java.lang.ArithmeticException: / by zero
at org.hu.test.controller.Test.main(Test.java:41) */
}
}
}
我們可以通過異常對象提供三種方法來查看異常信息。
- getMessage() :異常信息
- toString():異常類型 + 異常信息
- printStackTrace():異常類型 + 異常信息 + 出現問題行號
不難發現,平常控制檯打印的報錯信息,就是JVM調用printStackTrace()方法而打印的信息。
2.2 finally
finally關鍵字無法單獨使用,必須要配合try才能使用。使用finally關鍵字大多數情況下是用來釋放資源的,例如關閉數據庫連接。出現finally關鍵字,不管try代碼塊中是否出現異常,都一定會執行finally代碼塊中的代碼。下面看這樣一段代碼:
public class Test {
public static void main(String[] args) throws Exception {
try {
System.out.println(1/0);
} catch (Exception e) {
System.out.println("catch執行");
return;
} finally {
System.out.println("finally執行");
}
}
}
打印結果會不會有點喫驚?是的。即使catch代碼塊中出現了return,finally代碼塊中的語句還是執行了。這就是爲什麼上面說finally關鍵字一般是用來釋放資源的,因爲不管是否出現異常,和數據庫的連接一定要關閉,finally關鍵字就保證最後一定會執行關閉數據庫連接的語句。
但是如果出現下面這種情況finally代碼塊中的語句就不會被執行:
public class Test {
public static void main(String[] args) throws Exception {
try {
System.out.println(1/0);
} catch (Exception e) {
System.out.println("catch執行");
System.exit(0); // 退出JVM
} finally {
System.out.println("finally執行");
}
}
}
下面再看這樣一段代碼,猜猜看return返回的結果是多少:
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(getNum());
}
public static int getNum () {
int x = 10;
try {
System.out.println(1/0);
return x;
} catch (Exception e) {
x =20;
return x;
} finally {
x = 30;
System.out.println("finally執行");
}
}
}
return返回的結果是20。首先try代碼塊中的return是不會執行的,那麼執行的return語句就是catch代碼塊中return。問題是finally中的語句確實執行了,但是返回x的值沒有改變。其實當執行到catch代碼塊中的return語句的時候,返回值x就已經確定下來了,再去執行finally代碼塊中的語句的時候,已經無法改變返回值了。
自定義異常
除了系統定義的異常類,我們也可以自定義異常類。看下面一個例子:
public class AgeOutofBoundException extends Exception {
public AgeOutofBoundException () {
super();
}
public AgeOutofBoundException (String message) {
super(message);
}
}
public class Person {
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) throws AgeOutofBoundException {
if (age > 0 && age < 200) {
this.age = age;
} else {
throw new AgeOutofBoundException("/年齡錯誤");
}
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.setName("張三");
p.setAge(-18);
System.out.println(p);
}
}
通過控制檯的打印信息可以發現,除了異常類名變成了我們自定義的異常類,其餘的打印信息和平時出現的異常信息沒有區別。其實我們需要的恰恰就是異常類名,當看到AgeOutofBoundException異常出現的時候,就可以很輕鬆的知道是在年齡使用上出現了問題。
繼承與異常
如果子類重寫父類的方法,並且父類方法拋出異常,那麼
- 子類方法拋出異常類型必須是父類方法拋出異常類型的子類或者和父類拋出異常的類型一致(子類方法也可不拋出異常)
- 子類方法拋出異常的數量不得超過父類方法拋出異常的數量
- 父類方法沒有拋出異常,子類不能拋出異常
class Fu {
public void print() throws FileNotFoundException {
System.out.println("Fu");
}
public void print2() throws FileNotFoundException {
System.out.println("Fu");
}
}
class Zi extends Fu {
/*public void print() throws Exception {
System.out.println("Zi"); // 錯誤
}*/
public void print() throws FileNotFoundException { // 子類重寫父類方法,拋出異常類型必須和父類方法拋出異常類型一致或者是其子類
System.out.println("Zi");
}
public void print2() { // 不拋出異常
System.out.println("Zi");
}
}
class Fu {
public void print() {
System.out.println("Fu");
}
}
class Zi extends Fu{
@Override
public void print() { // 父類沒有拋出異常,子類不能拋出異常
try {
FileInputStream fs = new FileInputStream("E:\\test.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
tip:上面所說的異常都是編譯時異常,拋出運行時異常不必遵守上面的規則。
class Fu {
public void print() throws ArithmeticException {
System.out.println("Fu");
}
}
class Zi extends Fu {
public void print() throws ArrayIndexOutOfBoundsException, NullPointerException {
System.out.println("Zi");
}
}
參考: