一文搞懂Java異常
第8章 異常
本章學習目標
- 知道編譯時異常(受檢異常)與運行時異常(非受檢異常)
- 掌握常見的幾種異常或錯誤類型
- 掌握try-catch結構的語法格式和執行特點
- 掌握關鍵字finally的作用和特點
- 掌握關鍵字throw的作用
- 掌握關鍵字throws的作用
- 知道throw與throws的區別
- 了解Object類clone方法的重寫
8.1 異常概述
8.1.1 認識Java的異常
什么是異常
在使用計算機語言進行項目開發的過程中,即使程序員把代碼寫得盡善盡美,在系統的運行過程中仍然會遇到一些問題,因為很多問題不是靠代碼能夠避免的,比如:客戶輸入數據的格式問題,讀取文件是否存在,網絡是否始終保持通暢等等。
異常 :指的是程序在執行過程中,出現的非正常的情況,如果不處理最終會導致JVM的非正常停止。
異常指的并不是語法錯誤,語法錯了,編譯不通過,不會產生字節碼文件,根本不能運行.
異常也不是指邏輯代碼錯誤而沒有得到想要的結果,例如:求a與b的和,你寫成了a-b
如何對待異常
程序員在編寫程序時,就應該充分考慮到各種可能發生的異常和錯誤,極力預防和避免,實在無法避免的,要編寫相應的代碼進行異常的檢測、異常消息的提示,以及異常的處理。
異常的拋出機制
Java中是如何表示不同的異常情況,又是如何讓程序員得知,并處理異常的呢?
Java中把不同的異常用不同的類表示,一旦發生某種異常,就通過創建該異常類型的對象,并且拋出,然后程序員可以catch到這個異常對象,并處理,如果無法catch到這個異常對象,那么這個異常對象將會導致程序終止。
運行下面的程序,程序會產生一個數組索引越界異常ArrayIndexOfBoundsException。我們通過圖解來解析下異常產生和拋出的過程。
工具類
public class ArrayTools { // 對給定的數組通過給定的角標獲取元素。 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"); } }
上述程序執行過程圖解:
8.1.2 Java異常體系
Throwable
`java.lang.Throwable` 類是Java語言中所有錯誤或異常的超類。
只有當對象是此類(或其子類之一)的實例時,才能通過Java 虛擬機或者Java的`throw` 語句拋出。類似地,只有此類或其子類之一才可以是 `catch` 子句中的參數類型。
Error和Exception
`Throwable`有兩個直接子類:`java.lang.Error`與`java.lang.Exception`,平常所說的異常指`java.lang.Exception`。
Error:表示嚴重錯誤,一旦發生必須停下來查看問題并解決問題才能繼續,無法僅僅通過try...catch解決的錯誤。(如果拿生病做比喻,就像是突發疾病,而且是危重癥,必須立刻停下來治療而不是靠短暫休息、吃藥、打針、或小手術簡單解決處理)
例如:StackOverflowError(棧內存溢出)和OutOfMemoryError(堆內存溢出,簡稱OOM)。
Exception:表示普通異常,其它因編程錯誤或偶然的外在因素導致的一般性問題,程序員可以通過代碼的方式檢測、提示和糾正,使程序繼續運行,但是只要發生也是必須處理,否則程序也會掛掉。(這就好比普通感冒、闌尾炎、牙疼等,可以通過短暫休息、吃藥、打針、或小手術簡單解決,但是也不能擱置不處理,不然也會要人命)。
例如:空指針訪問、試圖讀取不存在的文件、網絡連接中斷、數組下標越界等
無論是Error還是Exception,還有很多子類,異常的類型非常豐富。當代碼運行出現異常時,特別是我們不熟悉的異常時,不要緊張,把異常的簡單類名,拷貝到API中去查去認識它即可。
8.1.3 受檢異常和非受檢異常
我們平常說的異常就是指Exception,根據代碼的編寫編譯階段,編譯器是否會警示當前代碼可能發生xx異常,并督促程序員提前編寫處理它的代碼為依據,可以將異常分為:
- 編譯時期異常(即checked異常、受檢異常):在代碼編譯階段,編譯器就能明確警示當前代碼可能發生(不是一定發生)xx異常,并督促程序員提前編寫處理它的代碼。如果程序員不聽話,沒有編寫對應的異常處理代碼,則編譯器就會發威,直接判定編譯失敗,從而程序無法執行。通常,這類異常的發生不是由程序員的代碼引起的,或者不是靠加簡單判斷就可以避免的,例如:FileNotFoundException(文件找不到異常)。
- 運行時期異常(即runtime異常、unchecked非受檢異常):即在代碼編譯階段,編譯器完全不做任何檢查,無論該異常是否會發生,編譯器都不給出任何提示。只有等代碼運行起來并確實發生了xx異常,它才能被發現。通常,這類異常是由程序員的代碼編寫不當引起的,只要稍加判斷,或者細心檢查就可以避免的。例如:ArrayIndexOutOfBoundsException數組下標越界異常,ClassCastException類型轉換異常。

8.1.4 演示常見的錯誤和異常
Error
最常見的就是VirtualMachineError,它有兩個經典的子類: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("請輸入一個整數:");//輸入非整數 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學習秘籍.txt");//編譯報錯 } }
8.2 異常的處理
Java異常處理的五個關鍵字:try、catch、finally、throw、throws
8.2.1 捕獲異常:try…catch
當某段代碼可能發生異常,不管這個異常是編譯時異常(受檢異常)還是運行時異常(非受檢異常),我們都可以使用try塊將它括起來,并在try塊下面編寫catch分支嘗試捕獲對應的異常對象。
try...catch語法格式:
try{
可能發生xx異常的代碼
}catch(異常類型1 e){
處理異常的代碼1
}catch(異常類型2 e){
處理異常的代碼2
}
....
try{
可能發生xx異常的代碼
}catch(異常類型1 | 異常類型2 e){
處理異常的代碼1
}catch(異常類型3 e){
處理異常的代碼2
}
....
- try{}中編寫可能發生xx異常的業務邏輯代碼。
- catch分支,分為兩個部分,catch()中編寫異常類型和異常參數名,{}中編寫如果發生了這個異常,要做什么處理的代碼。如果有多個catch分支,并且多個異常類型有父子類關系,必須保證小的子異常類型在上,大的父異常類型在下。
- 在catch分支中如何獲取異常信息,Throwable類中定義了一些查看方法:
- `public String getMessage()`:獲取異常的描述信息,原因(提示給用戶的時候,就提示錯誤原因。
- `public void printStackTrace()`:打印異常的跟蹤棧信息并輸出到控制臺。
包含了異常的類型,異常的原因,還包括異常出現的位置,在開發和調試階段,都得使用printStackTrace。
- 執行流程
- 如果在程序運行時,try塊中的代碼沒有發生異常,那么catch所有的分支都不執行。
- 如果在程序運行時,try塊中的代碼發生了異常,根據異常對象的類型,將從上到下選擇第一個匹配的catch分支執行。此時try中發生異常的語句下面的代碼將不執行,而整個try...catch之后的代碼可以繼續運行。
- 如果在程序運行時,try塊中的代碼發生了異常,但是所有catch分支都無法匹配(捕獲)這個異常,那么JVM將會終止當前方法的執行,并把異常對象“拋”給調用者。如果調用者不處理,程序就掛了。
示例代碼:
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("請輸入一個正整數:");
m = input.nextInt();
if (m < 0) {
System.out.println("輸入有誤," + m + "不是正整數!");
} else {
break;
}
} catch (InputMismatchException e) {
//String result = input.nextLine();
//System.out.println("輸入有誤," + result + "不是整數");
e.printStackTrace();
}
}
System.out.println("m = " + m);
}
}
8.2.2 finally塊
finally塊
因為異常會引發程序跳轉,從而會導致有些語句執行不到。而程序中有一些特定的代碼無論異常是否發生,都需要執行。例如,IO流的關閉,數據庫連接的斷開等。這樣的代碼通常就會放到finally塊中。
try{ }catch(...){ }finally{ 無論try中是否發生異常,也無論catch是否捕獲異常,也不管try和catch中是否有return語句,都一定會執行 } 或 try{ }finally{ 無論try中是否發生異常,也不管try中是否有return語句,都一定會執行。 }
注意:finally不能單獨使用。
當只有在try或者catch中調用退出JVM的相關方法,例如System.exit(0),此時finally才不會執行,否則finally永遠會執行。
示例代碼:
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("請輸入第一個整數:"); int a = input.nextInt(); System.out.print("請輸入第二個整數:"); int b = input.nextInt(); int result = a / b; System.out.println(a + "/" + b + "=" + result); } catch (InputMismatchException e) { System.out.println("數字格式不正確,請輸入兩個整數"); } catch (ArithmeticException e) { System.out.println("第二個整數不能為0"); } finally { System.out.println("程序結束,釋放資源"); 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結束"); } } }
形式二:從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結束"); } } }
形式三:從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結束"); return 0; } } }
8.2.3 手工拋出異常對象:throw
- 異常對象生成的兩種方式
- 由虛擬機自動生成:程序運行過程中,虛擬機檢測到程序發生了問題,就會在后臺自動創建一個對應異常類的實例對象并拋出——自動拋出。適用于核心類庫中預定義的異常類型。
- 由開發人員手動創建:new 異常類型(【實參列表】);,如果創建好的異常對象不拋出對程序沒有任何影響,和創建一個普通對象一樣,但是一旦throw拋出,就會對程序運行產生影響了。適用于預定義類型和自定義異常。
throw異常對象的語法格式
throw new 異常類名(【參數】);
throw語句拋出的異常對象,和JVM自動創建和拋出的異常對象一樣,需要處理。如果沒有被try..catch合理的處理,也會導致程序崩潰。
throw語句會導致程序執行流程被改變,throw語句是明確拋出一個異常對象,因此它下面的代碼將不會執行,如果當前方法沒有try...catch處理這個異常對象,throw語句就會代替return語句提前終止當前方法的執行,并返回一個異常對象給調用者。
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("沒有傳入任何整數,無法獲取最大值"); } 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編譯時異常
如果在編寫方法體的代碼時,某句代碼可能發生某個編譯時異常,不處理編譯不通過,但是在當前方法體中可能不適合處理或無法給出合理的處理方式,就可以通過throws在方法簽名中聲明該方法可能會發生xx異常,需要調用者處理。
聲明異常格式:
修飾符 返回值類型 方法名(參數) 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("三角形的邊長必須是正數,不能為負數"); } if (a + b <= c || b + c <= a || a + c <= b) { throw new Exception(a + "," + b + "," + c + "不能構造三角形,三角形任意兩邊之后必須大于第三邊"); } 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創建成功:" + t1); } catch (Exception e) { System.err.println("三角形1創建失敗"); e.printStackTrace(); } try { Triangle t2 = new Triangle(1, 1, 3); System.out.println("三角形2創建成功:" + t2); } catch (Exception e) { System.err.println("三角形2創建失敗"); e.printStackTrace(); } } }
throws運行時異常
當然,throws后面也可以寫運行時異常類型,只是運行時異常類型,寫或不寫對于編譯器和程序執行來說都沒有任何區別。如果寫了,唯一的區別就是調用者調用該方法后,使用try...catch結構時,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要求
方法重寫時,對于方法簽名是有嚴格要求的:
- 方法名必須相同
- 形參列表必須相同
- 返回值類型
- 基本數據類型和void:必須相同
- 引用數據類型:<=
- 權限修飾符:>=,而且要求父類被重寫方法在子類中是可見的
- 不能是static,final修飾的方法
- throws異常列表要求
- 如果父類被重寫方法的方法簽名后面沒有 “throws 編譯時異常類型”,那么重寫方法時,方法簽名后面也不能出現“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
所有類型都可以重寫這個方法,它是獲取一個對象的克隆體對象用的,就是造一個和當前對象各種屬性值一模一樣的對象。當然地址肯定不同。
我們在重寫這個方法后時,調用super.clone(),發現報異常CloneNotSupportedException,因為我們沒有實現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); } }