一文搞懂Java異常
第8章 異常
本章學(xué)習(xí)目標
- 知道編譯時異常(受檢異常)與運行時異常(非受檢異常)
- 掌握常見的幾種異常或錯誤類型
- 掌握try-catch結(jié)構(gòu)的語法格式和執(zhí)行特點
- 掌握關(guān)鍵字finally的作用和特點
- 掌握關(guān)鍵字throw的作用
- 掌握關(guān)鍵字throws的作用
- 知道throw與throws的區(qū)別
- 了解Object類clone方法的重寫
8.1 異常概述
8.1.1 認識Java的異常
什么是異常
在使用計算機語言進行項目開發(fā)的過程中,即使程序員把代碼寫得盡善盡美,在系統(tǒng)的運行過程中仍然會遇到一些問題,因為很多問題不是靠代碼能夠避免的,比如:客戶輸入數(shù)據(jù)的格式問題,讀取文件是否存在,網(wǎng)絡(luò)是否始終保持通暢等等。
異常 :指的是程序在執(zhí)行過程中,出現(xiàn)的非正常的情況,如果不處理最終會導(dǎo)致JVM的非正常停止。
異常指的并不是語法錯誤,語法錯了,編譯不通過,不會產(chǎn)生字節(jié)碼文件,根本不能運行.
異常也不是指邏輯代碼錯誤而沒有得到想要的結(jié)果,例如:求a與b的和,你寫成了a-b
如何對待異常
程序員在編寫程序時,就應(yīng)該充分考慮到各種可能發(fā)生的異常和錯誤,極力預(yù)防和避免,實在無法避免的,要編寫相應(yīng)的代碼進行異常的檢測、異常消息的提示,以及異常的處理。
異常的拋出機制
Java中是如何表示不同的異常情況,又是如何讓程序員得知,并處理異常的呢?
Java中把不同的異常用不同的類表示,一旦發(fā)生某種異常,就通過創(chuàng)建該異常類型的對象,并且拋出,然后程序員可以catch到這個異常對象,并處理,如果無法catch到這個異常對象,那么這個異常對象將會導(dǎo)致程序終止。
運行下面的程序,程序會產(chǎn)生一個數(shù)組索引越界異常ArrayIndexOfBoundsException。我們通過圖解來解析下異常產(chǎn)生和拋出的過程。
工具類
public class ArrayTools { // 對給定的數(shù)組通過給定的角標獲取元素。 public static int getElement(int[] arr, int index) { int element = arr[index]; return element; } }
測試類
public class ExceptionDemo { public static void main(String[] args) { int[] arr = { 34, 12, 67 }; int num = ArrayTools.getElement(arr, 4); System.out.println("num=" + num); System.out.println("over"); } }
上述程序執(zhí)行過程圖解:
8.1.2 Java異常體系
Throwable
`java.lang.Throwable` 類是Java語言中所有錯誤或異常的超類。
只有當對象是此類(或其子類之一)的實例時,才能通過Java 虛擬機或者Java的`throw` 語句拋出。類似地,只有此類或其子類之一才可以是 `catch` 子句中的參數(shù)類型。
Error和Exception
`Throwable`有兩個直接子類:`java.lang.Error`與`java.lang.Exception`,平常所說的異常指`java.lang.Exception`。
Error:表示嚴重錯誤,一旦發(fā)生必須停下來查看問題并解決問題才能繼續(xù),無法僅僅通過try...catch解決的錯誤。(如果拿生病做比喻,就像是突發(fā)疾病,而且是危重癥,必須立刻停下來治療而不是靠短暫休息、吃藥、打針、或小手術(shù)簡單解決處理)
例如:StackOverflowError(棧內(nèi)存溢出)和OutOfMemoryError(堆內(nèi)存溢出,簡稱OOM)。
Exception:表示普通異常,其它因編程錯誤或偶然的外在因素導(dǎo)致的一般性問題,程序員可以通過代碼的方式檢測、提示和糾正,使程序繼續(xù)運行,但是只要發(fā)生也是必須處理,否則程序也會掛掉。(這就好比普通感冒、闌尾炎、牙疼等,可以通過短暫休息、吃藥、打針、或小手術(shù)簡單解決,但是也不能擱置不處理,不然也會要人命)。
例如:空指針訪問、試圖讀取不存在的文件、網(wǎng)絡(luò)連接中斷、數(shù)組下標越界等
無論是Error還是Exception,還有很多子類,異常的類型非常豐富。當代碼運行出現(xiàn)異常時,特別是我們不熟悉的異常時,不要緊張,把異常的簡單類名,拷貝到API中去查去認識它即可。
8.1.3 受檢異常和非受檢異常
我們平常說的異常就是指Exception,根據(jù)代碼的編寫編譯階段,編譯器是否會警示當前代碼可能發(fā)生xx異常,并督促程序員提前編寫處理它的代碼為依據(jù),可以將異常分為:
- 編譯時期異常(即checked異常、受檢異常):在代碼編譯階段,編譯器就能明確警示當前代碼可能發(fā)生(不是一定發(fā)生)xx異常,并督促程序員提前編寫處理它的代碼。如果程序員不聽話,沒有編寫對應(yīng)的異常處理代碼,則編譯器就會發(fā)威,直接判定編譯失敗,從而程序無法執(zhí)行。通常,這類異常的發(fā)生不是由程序員的代碼引起的,或者不是靠加簡單判斷就可以避免的,例如:FileNotFoundException(文件找不到異常)。
- 運行時期異常(即runtime異常、unchecked非受檢異常):即在代碼編譯階段,編譯器完全不做任何檢查,無論該異常是否會發(fā)生,編譯器都不給出任何提示。只有等代碼運行起來并確實發(fā)生了xx異常,它才能被發(fā)現(xiàn)。通常,這類異常是由程序員的代碼編寫不當引起的,只要稍加判斷,或者細心檢查就可以避免的。例如:ArrayIndexOutOfBoundsException數(shù)組下標越界異常,ClassCastException類型轉(zhuǎn)換異常。
8.1.4 演示常見的錯誤和異常
Error
最常見的就是VirtualMachineError,它有兩個經(jīng)典的子類:StackOverflowError、OutOfMemoryError。
package com.atguigu.exception; import org.junit.Test; public class TestStackOverflowError { @Test public void test01() { //StackOverflowError digui(); } public void digui() { digui(); } }
package com.atguigu.exception; import org.junit.Test; public class TestOutOfMemoryError { @Test public void test02() { //OutOfMemoryError //方式一: int[] arr = new int[Integer.MAX_VALUE]; } @Test public void test03() { //OutOfMemoryError //方式二: StringBuilder s = new StringBuilder(); while (true) { s.append("atguigu"); } } }
非受檢的運行時異常
package com.atguigu.exception; import org.junit.Test; import java.util.Scanner; public class TestRuntimeException { @Test public void test01() { //NullPointerException int[][] arr = new int[3][]; System.out.println(arr[0].length); } @Test public void test02() { //ClassCastException Object obj = 15; String str = (String) obj; } @Test public void test03() { //ArrayIndexOutOfBoundsException int[] arr = new int[5]; for (int i = 1; i <= 5; i++) { System.out.println(arr[i]); } } @Test public void test04() { //InputMismatchException Scanner input = new Scanner(System.in); System.out.print("請輸入一個整數(shù):");//輸入非整數(shù) int num = input.nextInt(); input.close(); } @Test public void test05() { int a = 1; int b = 0; //ArithmeticException System.out.println(a / b); } }
受檢的編譯時異常
package com.atguigu.exception; import org.junit.Test; import java.io.FileInputStream; public class TestCheckedException { @Test public void test06() { Thread.sleep(1000);//休眠1秒,編譯報錯 } @Test public void test07() { FileInputStream fis = new FileInputStream("Java學(xué)習(xí)秘籍.txt");//編譯報錯 } }
8.2 異常的處理
Java異常處理的五個關(guān)鍵字:try、catch、finally、throw、throws
8.2.1 捕獲異常:try…catch
當某段代碼可能發(fā)生異常,不管這個異常是編譯時異常(受檢異常)還是運行時異常(非受檢異常),我們都可以使用try塊將它括起來,并在try塊下面編寫catch分支嘗試捕獲對應(yīng)的異常對象。
try...catch語法格式:
try{
可能發(fā)生xx異常的代碼
}catch(異常類型1 e){
處理異常的代碼1
}catch(異常類型2 e){
處理異常的代碼2
}
....
try{
可能發(fā)生xx異常的代碼
}catch(異常類型1 | 異常類型2 e){
處理異常的代碼1
}catch(異常類型3 e){
處理異常的代碼2
}
....
- try{}中編寫可能發(fā)生xx異常的業(yè)務(wù)邏輯代碼。
- catch分支,分為兩個部分,catch()中編寫異常類型和異常參數(shù)名,{}中編寫如果發(fā)生了這個異常,要做什么處理的代碼。如果有多個catch分支,并且多個異常類型有父子類關(guān)系,必須保證小的子異常類型在上,大的父異常類型在下。
- 在catch分支中如何獲取異常信息,Throwable類中定義了一些查看方法:
- `public String getMessage()`:獲取異常的描述信息,原因(提示給用戶的時候,就提示錯誤原因。
- `public void printStackTrace()`:打印異常的跟蹤棧信息并輸出到控制臺。
包含了異常的類型,異常的原因,還包括異常出現(xiàn)的位置,在開發(fā)和調(diào)試階段,都得使用printStackTrace。
- 執(zhí)行流程
- 如果在程序運行時,try塊中的代碼沒有發(fā)生異常,那么catch所有的分支都不執(zhí)行。
- 如果在程序運行時,try塊中的代碼發(fā)生了異常,根據(jù)異常對象的類型,將從上到下選擇第一個匹配的catch分支執(zhí)行。此時try中發(fā)生異常的語句下面的代碼將不執(zhí)行,而整個try...catch之后的代碼可以繼續(xù)運行。
- 如果在程序運行時,try塊中的代碼發(fā)生了異常,但是所有catch分支都無法匹配(捕獲)這個異常,那么JVM將會終止當前方法的執(zhí)行,并把異常對象“拋”給調(diào)用者。如果調(diào)用者不處理,程序就掛了。
示例代碼:
package com.atguigu.test;
import java.util.Scanner;
public class TestTryCatch1 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int m;
while (true) {
try {
System.out.print("請輸入一個正整數(shù):");
m = input.nextInt();
if (m < 0) {
System.out.println("輸入有誤," + m + "不是正整數(shù)!");
} else {
break;
}
} catch (InputMismatchException e) {
//String result = input.nextLine();
//System.out.println("輸入有誤," + result + "不是整數(shù)");
e.printStackTrace();
}
}
System.out.println("m = " + m);
}
}
8.2.2 finally塊
finally塊
因為異常會引發(fā)程序跳轉(zhuǎn),從而會導(dǎo)致有些語句執(zhí)行不到。而程序中有一些特定的代碼無論異常是否發(fā)生,都需要執(zhí)行。例如,IO流的關(guān)閉,數(shù)據(jù)庫連接的斷開等。這樣的代碼通常就會放到finally塊中。
try{ }catch(...){ }finally{ 無論try中是否發(fā)生異常,也無論catch是否捕獲異常,也不管try和catch中是否有return語句,都一定會執(zhí)行 } 或 try{ }finally{ 無論try中是否發(fā)生異常,也不管try中是否有return語句,都一定會執(zhí)行。 }
注意:finally不能單獨使用。
當只有在try或者catch中調(diào)用退出JVM的相關(guān)方法,例如System.exit(0),此時finally才不會執(zhí)行,否則finally永遠會執(zhí)行。
示例代碼:
package com.atguigu.keyword; import java.util.InputMismatchException; import java.util.Scanner; public class TestFinally { public static void main(String[] args) { Scanner input = new Scanner(System.in); try { System.out.print("請輸入第一個整數(shù):"); int a = input.nextInt(); System.out.print("請輸入第二個整數(shù):"); int b = input.nextInt(); int result = a / b; System.out.println(a + "/" + b + "=" + result); } catch (InputMismatchException e) { System.out.println("數(shù)字格式不正確,請輸入兩個整數(shù)"); } catch (ArithmeticException e) { System.out.println("第二個整數(shù)不能為0"); } finally { System.out.println("程序結(jié)束,釋放資源"); input.close(); } } }
finally與return
finally中寫了return語句,那么try和catch中的return語句就失效了,最終返回的是finally塊中的
形式一:從try回來
public class TestReturn { public static void main(String[] args) { int result = test("12"); System.out.println(result); } public static int test(String str) { try { Integer.parseInt(str); return 1; } catch (NumberFormatException e) { return -1; } finally { System.out.println("test結(jié)束"); } } }
形式二:從catch回來
public class TestReturn { public static void main(String[] args) { int result = test("a"); System.out.println(result); } public static int test(String str) { try { Integer.parseInt(str); return 1; } catch (NumberFormatException e) { return -1; } finally { System.out.println("test結(jié)束"); } } }
形式三:從finally回來
public class TestReturn { public static void main(String[] args) { int result = test("a"); System.out.println(result); } public static int test(String str) { try { Integer.parseInt(str); return 1; } catch (NumberFormatException e) { return -1; } finally { System.out.println("test結(jié)束"); return 0; } } }
8.2.3 手工拋出異常對象:throw
- 異常對象生成的兩種方式
- 由虛擬機自動生成:程序運行過程中,虛擬機檢測到程序發(fā)生了問題,就會在后臺自動創(chuàng)建一個對應(yīng)異常類的實例對象并拋出——自動拋出。適用于核心類庫中預(yù)定義的異常類型。
- 由開發(fā)人員手動創(chuàng)建:new 異常類型(【實參列表】);,如果創(chuàng)建好的異常對象不拋出對程序沒有任何影響,和創(chuàng)建一個普通對象一樣,但是一旦throw拋出,就會對程序運行產(chǎn)生影響了。適用于預(yù)定義類型和自定義異常。
throw異常對象的語法格式
throw new 異常類名(【參數(shù)】);
throw語句拋出的異常對象,和JVM自動創(chuàng)建和拋出的異常對象一樣,需要處理。如果沒有被try..catch合理的處理,也會導(dǎo)致程序崩潰。
throw語句會導(dǎo)致程序執(zhí)行流程被改變,throw語句是明確拋出一個異常對象,因此它下面的代碼將不會執(zhí)行,如果當前方法沒有try...catch處理這個異常對象,throw語句就會代替return語句提前終止當前方法的執(zhí)行,并返回一個異常對象給調(diào)用者。
package com.atguigu.throwdemo; public class TestThrow { public static void main(String[] args) { try { System.out.println(max(4, 2, 31, 1)); } catch (Exception e) { e.printStackTrace(); } try { System.out.println(max(4)); } catch (Exception e) { e.printStackTrace(); } try { System.out.println(max()); } catch (Exception e) { e.printStackTrace(); } } public static int max(int... nums) { if (nums == null || nums.length == 0) { throw new IllegalArgumentException("沒有傳入任何整數(shù),無法獲取最大值"); } int max = nums[0]; for (int i = 1; i < nums.length; i++) { if (nums[i] > max) { max = nums[i]; } } return max; } }
8.2.4 聲明方法可能拋出的異常:throws
throws編譯時異常
如果在編寫方法體的代碼時,某句代碼可能發(fā)生某個編譯時異常,不處理編譯不通過,但是在當前方法體中可能不適合處理或無法給出合理的處理方式,就可以通過throws在方法簽名中聲明該方法可能會發(fā)生xx異常,需要調(diào)用者處理。
聲明異常格式:
修飾符 返回值類型 方法名(參數(shù)) throws 異常類名1,異常類名2…{ }
在throws后面可以寫多個異常類型,用逗號隔開。
package com.atguigu.test; public class Triangle { private final double a; private final double b; private final double c; public Triangle(double a, double b, double c) throws Exception { if (a <= 0 || b <= 0 || c <= 0) { throw new Exception("三角形的邊長必須是正數(shù),不能為負數(shù)"); } if (a + b <= c || b + c <= a || a + c <= b) { throw new Exception(a + "," + b + "," + c + "不能構(gòu)造三角形,三角形任意兩邊之后必須大于第三邊"); } this.a = a; this.b = b; this.c = c; } public double getA() { return a; } public double getB() { return b; } public double getC() { return c; } @Override public String toString() { return "Triangle{" + "a=" + a + ", b=" + b + ", c=" + c + '}'; } }
package com.atguigu.test; public class TestThrows { public static void main(String[] args) { try { Triangle t1 = new Triangle(2, 2, 3); System.out.println("三角形1創(chuàng)建成功:" + t1); } catch (Exception e) { System.err.println("三角形1創(chuàng)建失敗"); e.printStackTrace(); } try { Triangle t2 = new Triangle(1, 1, 3); System.out.println("三角形2創(chuàng)建成功:" + t2); } catch (Exception e) { System.err.println("三角形2創(chuàng)建失敗"); e.printStackTrace(); } } }
throws運行時異常
當然,throws后面也可以寫運行時異常類型,只是運行時異常類型,寫或不寫對于編譯器和程序執(zhí)行來說都沒有任何區(qū)別。如果寫了,唯一的區(qū)別就是調(diào)用者調(diào)用該方法后,使用try...catch結(jié)構(gòu)時,IDEA可以獲得更多的信息,需要添加什么catch分支。
package com.atguigu.test; public class TestThrowsRuntimeException { public static void main(String[] args) { try { System.out.println(divide(1, 2)); } catch (ArithmeticException e) { throw new RuntimeException(e); } } public static int divide(int a, int b) throws ArithmeticException { return a / b; } }
8.3 方法重寫對于throws要求
方法重寫對于throws要求
方法重寫時,對于方法簽名是有嚴格要求的:
- 方法名必須相同
- 形參列表必須相同
- 返回值類型
- 基本數(shù)據(jù)類型和void:必須相同
- 引用數(shù)據(jù)類型:<=
- 權(quán)限修飾符:>=,而且要求父類被重寫方法在子類中是可見的
- 不能是static,final修飾的方法
- throws異常列表要求
- 如果父類被重寫方法的方法簽名后面沒有 “throws 編譯時異常類型”,那么重寫方法時,方法簽名后面也不能出現(xiàn)“throws 編譯時異常類型”。
- 如果父類被重寫方法的方法簽名后面有 “throws 編譯時異常類型”,那么重寫方法時,throws的編譯時異常類型必須<=被重寫方法throws的編譯時異常類型,或者不throws編譯時異常。
- 方法重寫,對于“throws 運行時異常類型”沒有要求。
package com.atguigu.keyword; import java.io.IOException; public class TestOverride { } class Father { public void method() throws Exception { System.out.println("Father.method"); } } class Son extends Father { @Override public void method() throws IOException, ClassCastException { System.out.println("Son.method"); } }
Object的clone方法和java.lang.Cloneable接口
在java.lang.Object類中有一個方法:
protected Object clone() throws CloneNotSupportedException
所有類型都可以重寫這個方法,它是獲取一個對象的克隆體對象用的,就是造一個和當前對象各種屬性值一模一樣的對象。當然地址肯定不同。
我們在重寫這個方法后時,調(diào)用super.clone(),發(fā)現(xiàn)報異常CloneNotSupportedException,因為我們沒有實現(xiàn)java.lang.Cloneable接口。
class Teacher implements Cloneable { private int id; private String name; public Teacher(int id, String name) { super(); this.id = id; this.name = name; } public Teacher() { super(); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Teacher [id=" + id + ", name=" + name + "]"; } @Override public Teacher clone() throws CloneNotSupportedException { return (Teacher) super.clone(); } }
public class TestClonable { public static void main(String[] args) throws CloneNotSupportedException { Teacher src = new Teacher(1, "柴老師"); Teacher clone = src.clone(); System.out.println(clone); System.out.println(src); System.out.println(src == clone); } }