JavaSE(九)——异常

异常

异常的概念

在Java中,将程序执行过程中发生的不正常行为称为异常。(不包含语法错误和逻辑错误)

Java中的异常,其实是一个一个的,可以是JVM自动抛出的,也可以是程序员通过throw语句手动抛出的。

我们举几个例子,并观察其源码:

  1. ArithmeticException算术异常

        public static void main(String[] args) {
            System.out.println(10 / 0);
        }
    

在这里插入图片描述
在这里插入图片描述

  1. NullPointerException空指针异常

    public static void main(String[] args) {
        int[] array = null;
        array[0] = 10;
    }
    

在这里插入图片描述
在这里插入图片描述


异常的体系结构

Java维护了一个异常的体系结构,如下图所示:

异常体系结构 - Hello图床

观察图片,我们得出:

  • Throwable是异常体系的顶层类其他所有的类都直接或间接地继承它,它派生出两个子类:ErrorException
  • Error是指Java虚拟机无法解决的严重问题,程序员是不能改变和处理的,遇到这样的问题,建议让程序直接终止,因此我们编写程序时不需要关心这一类错误。比如,JVM的内部错误、资源耗尽、线程死锁等。常见的:StackOverflowError栈溢出
  • Exception即我们平时常说的狭义的异常(后面讲解的异常均指Exception)表示程序可以处理的异常可以捕获并可以尝试恢复,所以遇到这类异常我们要尽力处理,使程序恢复运行。主要指编码、环境、用户操作输入出现问题

异常的分类

Java的异常(广义)根据Javac对异常的处理要求,分为 检查异常非检查异常

【检查异常】

检查异常即编译器要求必须处置的异常,其在编译期间抛出,如果我们不处置该类异常,代码将不会通过编译

检查异常 包含:除了ErrorRunTimeException类及其子类外的所有类

检查异常 通常采用try-catch捕获或者throws声明抛出的方式处理


【非检查异常】

非检查异常即不强制要求处置的异常,非检查异常不会在编译期间检查,而是在运行时抛出

非检查异常 包含:ErrorRunTimeException类及其子类

检查异常 不应该使用Java的异常处理机制来处置,而应该采用修改代码的方式(因为这些异常的出现大部分是代码本身出错),当然,我们可以使用try-catchthrows来处置它们


根据异常发生时机不同,可将Exception分为:编译时异常运行时异常

  1. 编译时异常

    在程序编译期间发生的异常,称为编译时异常,属于受检查异常(受查异常)。

    常见的就是克隆对象时的CloneNotSupportedException异常

    class Obj implements Cloneable {
        public int a;
        public String s;
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    public class TestException {
        public static void main(String[] args) {
            Obj obj = new Obj();
            Obj obj1 = (Obj)obj.clone();//红线提示
        }
    }
    

在这里插入图片描述

如图,受查异常会在运行前就有红线提示。(上代码解决方案是在main方法的参数列表后加上throws CloneNotSupportedException)

【注意】

不是所有的红线都是受查异常,例如我们将某个关键字写错或忘记写分号,这些都是基本的语法错误,不属于异常。

我们必须手动在代码里添加捕获语句来处理该异常,这类异常是我们主要处理的异常对象。

  1. 运行时异常

    在程序执行期间发生的异常,称为运行时异常,属于非受检查异常(非受查异常)

    RunTimeException及其子类对应的异常,称为运行时异常,如常见的 NullPointerExceptionArrayIndexOutBoundsException

    RunTimeException异常会由Java虚拟机自动抛出并自动捕获,此类异常出现的绝大多数情况是代码本身有问题,我们应该从逻辑上解决这些问题,修改我们的代码。


异常的抛出

这里我们介绍throw关键字。

前面提到,异常可以是JVM自动抛出的,也可以是程序员通过throw语句手动抛出的,throw一般用于抛出自定义异常(后面会讲)

throw语句用来抛出一个异常类对象,告知调用者相关的错误信息

【语法格式】

保证throw后面是异常类对象即可

  1. throw new 异常类名();

  2. 异常类名 引用变量名 = new 异常类名();

    throw 引用变量名;

我们以抛出非自定义异常为例,看一段代码:

public class TestException1 {
    
    public static void test() {
        int[] array = new int[]{1, 2, 3, 4, 5};
        System.out.println("请输入下标值:");
        Scanner scanner = new Scanner(System.in);
        int index = scanner.nextInt();
        if(index > 5 || index < 0) {
            throw new ArrayIndexOutOfBoundsException();
        }else {
            System.out.println(array[index]);
        }
        
    }
    public static void main(String[] args) {
        test();
    }
}

如上代码,我们输入 7 ,抛出ArrayIndexOutOfBoundsException异常

在这里插入图片描述

构造异常类时可以传入字符串来给出错误提示

public class TestException1 {

    public static void test(int x) {
        if(x == 0) {
            throw new ArithmeticException("将会出现10 / 0的不正确语法!");
        }else {
            System.out.println(10 / x);
        }
    }
    public static void main(String[] args) {
        test(0);//触发if语句手动抛出ArithmaticException异常并打印错误信息
    }
}

在这里插入图片描述

【注意事项】

  • throw语句只能位于方法体内
  • 如果抛出的异常是非检查异常,则可以不用处理,直接交给JVM处理,一旦交给JVM处理,程序将会终止报错
  • 如果抛出的异常是检查异常,则必须处理,否则编译不通过

异常的处理

异常的处理主要有两种方式:

一种是消极的处理方式throws声明抛出;另一种是try-catch捕获异常

throws

【语法格式】

修饰符 返回类型 方法名(参数列表) throws 异常类型1,异常类型2… {

}

当方法中抛出检查异常时,用户不想处理该异常,此时就可以使用throws将异常抛给方法的调用者来处理(当前方法不处理异常,提醒方法的调用者处理该异常

throws语句又叫 异常的声明,它告诉调用者调用此方法可能会抛出对应的异常。实际上并没有处理异常,而是交给了此方法的调用者(上层)来处理该异常,如果不断地throws,异常最终将会交给JVM处理,此时程序就会报错终止。


【举例】

//CloneObject.java
public class CloneObject implements Cloneable {
    private int a;
    private String s;

    public CloneObject(int a, String s) {
        this.a = a;
        this.s = s;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {  //将CloneNotSupportedException异常交给调用者,此处为main方法
        return super.clone();
    }
}

//TestException1.java
public class TestException1 {
    public static void main(String[] args) throws CloneNotSupportedException {  //main方法将异常抛给上层,即JVM
        CloneObject cloneObject1 = new CloneObject(12, "haha");
        CloneObject cloneObject2 = (CloneObject) cloneObject1.clone();
    }
}

【注意事项】

  • throws语句必须位于方法的参数列表后
  • throws后的异常必须是Exception类及其子类
  • 如果throws语句后面的多个异常存在父子关系,则只需要保留父类即可
  • throws语句处理异常的方式是消极的,它并没有实际处理异常,而是不断地将异常抛给其上层调用者,最终会抛给JVM处理

try-catch

throws没有真正处理异常,而是将异常抛给调用者,由调用者处理。try-catch 则是一种积极的处理异常的方式

【语法格式】

try {

} catch(异常类型 变量名) {

}

//后续代码…

try内部书写可能出现异常的代码,catch用于捕捉指定的异常,当try内部语句出现异常,catch语句将尝试捕获,如果异常类型匹配,则会捕获成功,执行catch内部的语句,执行完毕后,继续执行后续的语句;如果捕获失败(异常类型不匹配),程序将会直接终止并报错。

另外,try内部出现异常,出现异常的语句之后的语句将不会执行,直接跳转到catch语句尝试捕获

我们看一段代码:

    public static void main(String[] args) {
        int[] array = null;
        try {
            array[0] = 1;
            System.out.println("异常语句后的语句...");
        }catch (NullPointerException a) {
            System.out.println("执行了catch语句...");
        }
        System.out.println("执行了后续语句...");
    }

打印结果为:

在这里插入图片描述

分析: 我们创建了一个数组引用变量,它不指向任何对象(null),接着执行try内部语句,将会出现NullPointerException异常,此时后面的打印语句不会执行,直接跳转到catch语句尝试捕获,类型匹配,捕获成功,执行catch内部语句,最后执行后续语句,得到上图打印结果。


catch语句捕获到了异常,我们可能难以定位捕获异常的位置。

此时,我们可以通过printStackTrace方法打印追踪栈的信息,帮助我们进行定位

    public static void main(String[] args) {
        int[] array = null;
        try {
            array[0] = 1;
            System.out.println("异常语句后的语句...");
        }catch (NullPointerException a) {
            System.out.println("执行了catch语句...");
            a.printStackTrace();//打印错误信息
        }
        System.out.println("执行了后续语句...");
    }

在这里插入图片描述

这里有一个问题,printStackTrace方法在println之前,为什么先打印println的信息?

这是因为,printStackTrace内部的打印逻辑不同于println,现阶段不必追究,知道这一现象即可。


try中可能会抛出多个异常,此时要使用多个catch来一一捕获,防止某个异常没有被捕获

我们看一段代码:

    public static void main(String[] args) {
        int[] array = null;
        try {
            array[0] = 1;
            System.out.println(10 / 0);
            System.out.println("异常语句之后的语句...");
        }catch (NullPointerException e) {
            e.printStackTrace();
            System.out.println("捕获空指针异常的catch语句执行了...");
        }catch (ArithmeticException e) {
            e.printStackTrace();
            System.out.println("捕获算术异常的catch语句执行了...");
        }
        System.out.println("后续语句...");
    }

问: 此时会打印几个异常的错误信息?

在这里插入图片描述

打印结果显示,只打印了一个异常的错误信息,原因很简单,当try中出现了一个异常时,后面的语句将不再执行,也就是上述代码中会出现算术异常的10 / 0语句不会执行。

对于可能出现多个异常的情景,有两种方便但不推荐的写法:

  1. 仅存在一个catch语句且定义待捕获的类为Exception,通过一个catch语句捕获所有的异常

        public static void main(String[] args) {
            try {
                System.out.println("可能出现异常的语句...");
            }
            catch(Exception e) {
                System.out.println("捕获到了异常...");
            }
        }
    

    Exception是所有异常(狭义)类的父类,这种写法虽然方便,但是具体抛出了什么异常,我们不清楚,所以不推荐这样写。

    不过,我们可以在已有的多个catch语句的后面再加上一个Exception的catch语句以兜底。

        public static void main(String[] args) {
            int[] array = null;
            try {
                array[0] = 1;
                System.out.println(10 / 0);
                System.out.println("异常语句之后的语句...");
            }catch (NullPointerException e) {
                e.printStackTrace();
                System.out.println("捕获空指针异常的catch语句执行了...");
            }catch (ArithmeticException e) {
                e.printStackTrace();
                System.out.println("捕获算术异常的catch语句执行了...");
            }catch (Exception e) {
                e.printStackTrace();
                System.out.println("捕获到了异常...");
            }
            System.out.println("后续语句...");
        }
    
  2. 使用|连字符在一个catch语句定义多个异常

        public static void main(String[] args) {
            try {
                System.out.println("可能出现异常的语句...");
            }
            catch(NullPointerException | ArithmeticException e) {
                System.out.println("捕获到了异常...");
            }
            System.out.println("后续语句...");
        }
    

    与一个Exception一样,我们不清楚具体抛出的异常。我们只需要了解可以使用 |的语法即可


如果异常之间具有父子关系,则必须保证子类异常在前面的catch语句,父类异常在后面的catch语句

具体实例参考Exception兜底代码

在这里插入图片描述


【总结】

  • try块内出现异常位置后的语句不会执行
  • try中可能会抛出多个异常,此时要使用多个catch来一一捕获,防止某个异常没有被捕获,避免只使用一个catch语句捕获所有的异常以及使用|在一个catch语句中出现多个异常类
  • 如果catch语句的异常类出现了父子关系,则必须保证子类在上,父类在下,这是强制性的
  • 如果抛出异常的类型与catch不匹配,异常将会继续向外抛,知道抛给JVM中断程序

finally

编程过程中,有些代码,不论是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库、IO流等,在程序正常或异常退出时,必须对资源进行回收。另外,异常会引发程序的跳转,导致一些语句无法执行

针对这些问题,Java提供了finally关键字,它必须依附try-catch语句,即try-catch-finally

【语法格式】

try {

} catch(异常类型 变量名) {

} finally {

}

finally块内的语句一定会执行,不论是否发生异常、异常是否被捕获成功

问:finally语句与try-catch后面的语句都会执行,为什么还需要finally呢?

我们看一段代码:

public class TestException1 {

    public static int testFinally() {
        Scanner scanner = new Scanner(System.in);
        try {
            int ret = scanner.nextInt();
            return ret;
        }catch (InputMismatchException e) {
            e.printStackTrace();
        }finally {
            System.out.println("finally语句执行了...");
        }
        System.out.println("后续语句执行了...");
        scanner.close();
        return 0;
    }

    public static void main(String[] args) {
        System.out.println(testFinally());
    }
}

当输入的类型匹配时,例如输入 10:

在这里插入图片描述

此时,我们发现当没有抛出异常,try块内的代码都会执行,执行返回语句返回 10,同时finally的语句也会执行,但是后续语句没有执行,因为已经返回了。

当我们输入的类型不匹配时(即抛出了异常),如输入 “str”:

在这里插入图片描述

此时抛出异常被catch捕获,再执行finally和 后续语句。

通过上例,我们发现,异常引发的程序跳转使得后续代码可能执行,但是finally代码必定会执行,所以这就是finally语句出现的意义。

避免在finally中出现return语句

    public static int testFinallyAndReturn() {
        try {
            return 10;
        }catch (NullPointerException e) {
            e.printStackTrace();
        }finally {
            return 20;
        }
    }

    public static void main(String[] args) {
        System.out.println(testFinallyAndReturn());
    }

这段代码打印结果是什么?

在这里插入图片描述

  • finally执行的时机是在方法返回之前(如果try-catch中有return会在此return之前执行finally;但是如果finally中也存在return,那么就会执行finally中的return,从而不会执行到try-catch中原有的return

异常的处理流程

如果发生异常的方法中没有合适的异常处理方式,就会沿着调用栈向上(这里的上指的是调用者)传递。

方法之间是存在相互调用关系的,这种调用关系我们可以使用"调用栈"来描述。

JVM中有一块内存空间称为"虚拟机栈"专门存储方法之间的调用关系,前面介绍的printStackTrace方法就可以查看出现异常代码的调用栈

如上面举过例子的一段代码:

    public static int testFinally() {
        Scanner scanner = new Scanner(System.in);
        try {
            int ret = scanner.nextInt();
            return ret;
        }catch (InputMismatchException e) {
            e.printStackTrace();
        }finally {
            System.out.println("finally语句执行了...");
        }
        System.out.println("后续语句执行了...");
        scanner.close();
        return 0;
    }

    public static void main(String[] args) {
        System.out.println(testFinally());
    }

在这里插入图片描述

黄框内部显示了出现异常代码的调用栈,最上面是栈顶。当出现异常时,此异常会一直向栈底回溯,这种行为叫做异常的冒泡,如果传递过程中一直没有合适的处理方式,异常最终会交给JVM处理,程序就会异常终止。

【异常处理流程总结】

  • 程序先执行try中的代码
  • 如果try中的代码出现异常,就会结束try中的代码,catch开始尝试捕获(看异常类型是否匹配)
  • 如果捕获成功(异常类型匹配),就会执行catch中的代码;如果捕获失败(异常类型不匹配),异常就会向上传递到上层调用者,如果一直到main方法也没有合适的代码处理异常,就会交给JVM处理,此时程序就会异常终止
  • 无论是否捕获成功,finally代码都会执行

自定义异常类

Java中虽然已经内置了丰富的异常类,但是并不能完全表示实际开发中所遇到的一些异常,此时我们就需要自定义异常类,维护符合我们实际情况的异常结构。

观察Java内置的InputMismatchException异常源码:

在这里插入图片描述
在这里插入图片描述

观察框选部分,然后我们介绍规则。

【自定义异常的规则】

  • 自定义异常要继承ExceptionRunTimeException,继承自Exception默认为检查异常;继承自RunTimeException默认为非检查异常
  • 按照国际惯例,自定义异常类应该包含4个构造方法:
    1. 无参构造方法
    2. String参数的构造方法,并使用super传递给父类
    3. String参数 和 Throwable参数的构造方法,并使用super传递给父类
    4. Throwable参数的构造方法,并使用super传递给父类
  • 我们自定义异常类时,不必严格遵守国际惯例,只包含前两个无参和String参数的构造方法即可

了解完毕后,我们应用自定义异常实现段用户登录代码:

//UserNameException.java
public class UserNameException extends Exception {
    public UserNameException() {

    }

    public UserNameException(String s) {
        super(s);
    }
}
//PassWordException.java
public class PassWordException extends Exception {
    public PassWordException() {

    }

    public PassWordException(String s) {
        super(s);
    }
}
//Login.java
import java.util.Scanner;

public class Login {
    private String userName;
    private String passWord;

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public void loginInfo(String userName, String passWord) throws UserNameException, PassWordException {
        if(!this.userName.equals(userName)) {
            throw new UserNameException("用户名错误!");
        }
        if(!this.passWord.equals(passWord)) {
            throw new PassWordException("密码错误!");
        }

        System.out.println("登录成功!");
    }
}

class Test {
    public static void main(String[] args) {
        Login login = new Login();
        Scanner scanner = new Scanner(System.in);
        login.setUserName("abc");
        login.setPassWord("123");
        try {
            System.out.println("请输入用户名:");
            String userName = scanner.nextLine();
            System.out.println("请输入密码:");
            String passWord = scanner.nextLine();
            login.loginInfo(userName, passWord);
        }catch (UserNameException e) {
            System.out.println("用户名错误!");
            e.printStackTrace();
        }catch (PassWordException e) {
            System.out.println("密码错误!");
            e.printStackTrace();
        }finally {
            scanner.close();
        }
    }
}

熟悉了异常部分的知识,理解这些代码是容易的,希望大家多多练习!



JavaSE全篇马上就发布了!在此之前我们会补充一些知识:内部类Comparable接口和Comparator接口Object类

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值