一文搞懂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的異常

  1. 什么是異常

    在使用計算機語言進行項目開發(fā)的過程中,即使程序員把代碼寫得盡善盡美,在系統(tǒng)的運行過程中仍然會遇到一些問題,因為很多問題不是靠代碼能夠避免的,比如:客戶輸入數(shù)據(jù)的格式問題,讀取文件是否存在,網(wǎng)絡(luò)是否始終保持通暢等等。

    異常 :指的是程序在執(zhí)行過程中,出現(xiàn)的非正常的情況,如果不處理最終會導(dǎo)致JVM的非正常停止。

    異常指的并不是語法錯誤,語法錯了,編譯不通過,不會產(chǎn)生字節(jié)碼文件,根本不能運行.

    異常也不是指邏輯代碼錯誤而沒有得到想要的結(jié)果,例如:求a與b的和,你寫成了a-b

  2. 如何對待異常

    程序員在編寫程序時,就應(yīng)該充分考慮到各種可能發(fā)生的異常和錯誤,極力預(yù)防和避免,實在無法避免的,要編寫相應(yīng)的代碼進行異常的檢測、異常消息的提示,以及異常的處理。

  3. 異常的拋出機制

    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í)行過程圖解:

    1562772282750.png異常產(chǎn)生過程.png

8.1.2 Java異常體系

  1. Throwable

    `java.lang.Throwable` 類是Java語言中所有錯誤或異常的超類。

    只有當對象是此類(或其子類之一)的實例時,才能通過Java 虛擬機或者Java的`throw` 語句拋出。類似地,只有此類或其子類之一才可以是 `catch` 子句中的參數(shù)類型。

  2. 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中去查去認識它即可。

    簡單的異常查看.bmp

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)換異常。
1562771528807.png

8.1.4 演示常見的錯誤和異常

  1. 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");
                    }
                }
            }
  2. 非受檢的運行時異常

    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);
                }
            }
  3. 受檢的編譯時異常

    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
}
....
  1. try{}中編寫可能發(fā)生xx異常的業(yè)務(wù)邏輯代碼。
  2. catch分支,分為兩個部分,catch()中編寫異常類型和異常參數(shù)名,{}中編寫如果發(fā)生了這個異常,要做什么處理的代碼。如果有多個catch分支,并且多個異常類型有父子類關(guān)系,必須保證小的子異常類型在上,大的父異常類型在下。
  3. 在catch分支中如何獲取異常信息,Throwable類中定義了一些查看方法:
  • `public String getMessage()`:獲取異常的描述信息,原因(提示給用戶的時候,就提示錯誤原因。
  • `public void printStackTrace()`:打印異常的跟蹤棧信息并輸出到控制臺。

包含了異常的類型,異常的原因,還包括異常出現(xiàn)的位置,在開發(fā)和調(diào)試階段,都得使用printStackTrace。

  1. 執(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塊

  1. 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();
                    }
                }
            }

  2. 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

  1. 異常對象生成的兩種方式
    • 由虛擬機自動生成:程序運行過程中,虛擬機檢測到程序發(fā)生了問題,就會在后臺自動創(chuàng)建一個對應(yīng)異常類的實例對象并拋出——自動拋出。適用于核心類庫中預(yù)定義的異常類型。
    • 由開發(fā)人員手動創(chuàng)建:new 異常類型(【實參列表】);,如果創(chuàng)建好的異常對象不拋出對程序沒有任何影響,和創(chuàng)建一個普通對象一樣,但是一旦throw拋出,就會對程序運行產(chǎn)生影響了。適用于預(yù)定義類型和自定義異常。
  2. 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

  1. 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();
                    } 
                }
            }
  2. 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要求

  1. 方法重寫對于throws要求

    方法重寫時,對于方法簽名是有嚴格要求的:

    1. 方法名必須相同
    2. 形參列表必須相同
    3. 返回值類型
    • 基本數(shù)據(jù)類型和void:必須相同
    • 引用數(shù)據(jù)類型:<=
    1. 權(quán)限修飾符:>=,而且要求父類被重寫方法在子類中是可見的
    2. 不能是static,final修飾的方法
    3. 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");
                }
            }
  2. 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);
                }
            }