• Java:异常


    1.异常的概念

    在Java中,将程序执行过程中发生的不正常行为称为异常。
    1.算数异常

    System.out.println(10 / 0);
    
    • 1

    在这里插入图片描述
    2.数组越界异常

    int[] arr = {1, 2, 3};
            System.out.println(arr[100]);
    
    • 1
    • 2

    在这里插入图片描述
    3.空指针异常

    int[] arr = null;
    System.out.println(arr.length);
    
    • 1
    • 2

    在这里插入图片描述
    从上述过程中可以看到,java中不同类型的异常,都有与其对应的类来进行描述。

    2.异常的体系结构

    2.1异常主要结构体系

    异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构
    在这里插入图片描述

    从上图中可以看到:
    1.Throwable是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception
    2.Error指的是Java虚拟机无法解决的严重问题(错误),比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,一旦发生回力乏术(需要程序员手动去解决)。举例如下:

    public class Main {
        private static void func(){
            func();
        }
        public static void main(String[] args) {
            func();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    一直递归导致栈溢出错误StackOverflowError
    3.Exception异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception。异常又分为两大类(运行时异常(非受查异常)和非运行时异常(受查异常))。

    2.2异常的分类

    在这里插入图片描述
    红色方块为编译时异常,蓝色方块为运行时异常

    2.2.1 编译时异常

    在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception)

    class Person implements Cloneable{
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    public class Main {
        public static void main(String[] args) {
            Person person=new Person();
            Person person1=(Person)person.clone();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    程序还没有运行红色波浪号提出异常,就称为编译时异常,也叫受查异常。
    注意
    在这里插入图片描述
    这种情况并不能称作异常,只能叫做编译时的语法问题。

    2.2.2 运行时异常

    在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception) RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException。
    注意
    编译时出现的语法性错误,不能称之为异常。例如将 System.out.println 拼写错了, 写成了system.out.println. 此时编译过程中就会出错, 这是 “编译期” 出错。而运行时指的是程序已经编译通过得到class 文件了, 再由 JVM 执行过程中出现的错误,编译后报错如下:
    在这里插入图片描述

    3.异常的处理

    3.1 防御式编程

    错误在代码中是客观存在的. 因此我们要让程序出现问题的时候及时通知程序猿。主要的方式
    1.LBYL: Look Before You Leap. 在操作之前就做充分的检查. 即:事前防御型

    boolean ret = false;
     ret = 登陆游戏();
     if (!ret) {
        处理登陆游戏错误;
        return;
     }
     ret = 开始匹配();
     if (!ret) {
        处理匹配错误;
        return;
     }
     ret = 游戏确认();
     if (!ret) {
        处理游戏确认错误;
        return;
     }
     ret = 选择英雄();
     if (!ret) {
        处理选择英雄错误;
        return;
        }
     ret = 载入游戏画面();
     if (!ret) {
        处理载入游戏错误;
        return;
     }
     ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    缺陷:正常流程和错误处理流程代码混在一起, 代码整体显的比较混乱。
    2.** EAFP**: It’s Easier to Ask Forgiveness than Permission. “事后获取原谅比事前获取许可更容易”. 也就是先操作, 遇到问题再处理. 即:事后认错型

    try {
        登陆游戏();
        开始匹配();
        游戏确认();
        选择英雄();
        载入游戏画面();
        ...
     } catch (登陆游戏异常) {
        处理登陆游戏异常;
     } catch (开始匹配异常) {
        处理开始匹配异常;
     } catch (游戏确认异常) {
        处理游戏确认异常;
     } catch (选择英雄异常) {
        处理选择英雄异常;
     } catch (载入游戏画面异常) {
        处理载入游戏画面异常;
     }
     .....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    优势:正常流程和错误流程是分离开的, 程序员更关注正常流程,代码更清晰,容易理解代码异常处理的核心思想就是 EAFP。
    下面我们来介绍Java当中,异常处理主要的5个关键字:throw、try、catch、final、throws。

    3.2 异常的抛出

    在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者,比如:参数检测。
    在Java中,可以借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者。具体语法如下:

    throw new XXXException("异常产生的原因");
    
    • 1

    人为抛出异常举例如下

    public static void func(int[] array){
            if(array==null){
                throw new RuntimeException();
            }
        }
    
        public static void main(String[] args) {
            func(null);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果
    在这里插入图片描述
    当我们在throw时传个参数会发生什么呢

    public static void func(int[] array){
            if(array==null){
                throw new NullPointerException("传个参数试试");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    【注意事项】
    1.throw必须写在方法体内部
    2.抛出的对象必须是Exception 或者 Exception 的子类对象
    3.如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
    4.如果抛出的是编译时异常,用户必须处理,否则无法通过编译
    5.异常一旦抛出,其后的代码就不会执行

    怎么区分运行异常和编译异常?
    下面给出一个方法
    通过追溯到NullPointerException原码可以发现他继承RuntimeException,只要继承RuntimeException类都是运行时异常
    在这里插入图片描述
    而追溯到CloneNotSupportedException类原码可以知道他继承Exception,只要继承Exception类都是编译异常
    在这里插入图片描述

    3.3 异常的捕获

    异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理

    3.3.1 异常声明throws

    处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。

    语法格式:
    修饰符  返回值类型   方法名(参数列表) throws 异常类型1,异常类型2...{
    }
    
    • 1
    • 2
    • 3

    举例如下

    public static void func(int[] array){
            if(array==null){
                throw new Exception("传个参数试试");
            }
        }
        public static void main(String[] args) {
            func(null);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    用throws声明异常

    public static void func(int[] array) throws Exception{
            if(array==null){
                throw new Exception("传个参数试试");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但是调用func方法时还是会触发异常,解决方法看注意事项4和try-catch捕获并处理
    在这里插入图片描述

    【注意事项】
    1.throws必须跟在方法的参数列表之后
    2.声明的异常必须是 Exception 或者 Exception 的子类
    3.方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
    4.调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出。

    public static void main(String[] args) throws Exception {//继续抛出异常
            func(null);
        }
    
    • 1
    • 2
    • 3

    3.3.2 try-catch捕获并处理

    throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。

    public static void func(int[] array) throws Exception{
            if(array==null){
                throw new Exception("传个参数试试");
            }
        }
        public static void main(String[] args){
            try{//存放可能抛出异常的代码
                func(null);
            }catch (Exception e){
                System.out.println("捕获到了Exception异常!"+"此时可以处理这个异常了");
                e.printStackTrace();//定位异常位置
            }
            System.out.println("异常处理完毕,继续执行这个代码");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果如下
    在这里插入图片描述

    关于异常的处理方式
    异常的种类有很多, 我们要根据不同的业务场景来决定.
    对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果
    对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序猿
    对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试.
    在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.

    【注意事项】
    1.try块内抛出异常位置之后的代码将不会被执行

    public static void func(int[] array) throws Exception{
            if(array==null){
                throw new Exception("传个参数试试");
            }
        }
        public static void main(String[] args){
            try{//存放可能抛出异常的代码
                func(null);
                System.out.println("haha");//并不会执行
            }catch (Exception e){
                System.out.println("捕获到了Exception异常!"+"此时可以处理这个异常了");
                e.printStackTrace();//定位异常位置
            }
            System.out.println("异常处理完毕,继续执行这个代码");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    2.如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序----异常是按照类型来捕获的
    改上方代码为如下所示
    在这里插入图片描述
    此时抛出异常类型与catch时异常类型不匹配,程序直接终止了
    3.try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获----即多种异常,多次捕获

       public static void func(int[] array) throws Exception{
            if(array==null){
                throw new Exception("传个参数试试");
            }
        }
        public static void main(String[] args){
            try{//存放可能抛出异常的代码
                func(null);
                System.out.println("haha");//并不会执行
            }catch (ArithmeticException e){
                System.out.println("捕获到了ArithmeticException异常!");
                e.printStackTrace();//定位异常位置
            }catch (Exception e){//两个catch捕获异常
                System.out.println("捕获到了Exception异常");
            }
            System.out.println("异常处理完毕,继续执行这个代码");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    也可以将要捕获的异常通过|放在一个catch语句里面,但是不能放这些异常的父类(RuntimeException类和Exception类)。

    public static void func(int[] array) throws NullPointerException{
            if(array==null){
                throw new NullPointerException("传个参数试试");
            }
        }
        public static void main(String[] args){
            try{//存放可能抛出异常的代码
                func(null);
                System.out.println("haha");//并不会执行
            }catch (NullPointerException | ArithmeticException e){
                System.out.println("捕获到了异常!");
                e.printStackTrace();//定位异常位置
            }
            System.out.println("异常处理完毕,继续执行这个代码");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果
    在这里插入图片描述
    4.可以通过一个catch捕获所有的异常,即多个异常,一次捕获(不推荐)。catch语句里放异常的父类,举例如下

    public class Main {
        public static void func(int[] array) throws NullPointerException{
            if(array==null){
                throw new NullPointerException("传个参数试试");
            }
        }
        public static void main(String[] args){
            try{//存放可能抛出异常的代码
                func(null);
                System.out.println("haha");//并不会执行
            }catch (Exception e){
                System.out.println("捕获到了异常!");
                e.printStackTrace();//定位异常位置
            }
            System.out.println("异常处理完毕,继续执行这个代码");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    运行结果
    在这里插入图片描述

    3.3.3 finally

    在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
    不管是否发生异常,finally里面的语句一定会被执行

    public static void func(int[] array) throws NullPointerException{
            if(array==null){
                throw new NullPointerException("传个参数试试");
            }
        }
        public static void main(String[] args){
            Scanner scan=null;
            try{//存放可能抛出异常的代码
                scan=new Scanner(System.in);
                func(null);
                System.out.println("haha");//并不会执行
            }catch (NullPointerException e){
                System.out.println("捕获到了异常!");
                e.printStackTrace();//定位异常位置
            }finally {
                System.out.println("此条代码一定会被执行……");
                scan.close();//finally关闭Scanner,防止数据流失
            }
            System.out.println("异常未被捕捉,这条代码不会被执行");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行结果
    在这里插入图片描述
    问题:既然 finally 和 try-catch-finally 后的代码都会执行,那为什么还要有finally呢?
    eg:关闭输入流,防止资源泄露
    在这里插入图片描述
    注意:finally中的代码一定会执行的,一般在finally中进行一些资源清理的扫尾工作。

    4.异常处理流程

    关于 “调用栈”
    方法之间是存在相互调用关系的, 这种调用关系我们可以用 “调用栈” 来描述. 在 JVM 中有一块内存空间称为
    “虚拟机栈” 专门存储方法之间的调用关系. 当代码中出现异常的时候, 我们就可以使用 e.printStackTrace(); 的方式查看出现异常代码的调用栈。
    如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上传递

    public static void main(String[] args) {
        try {
            func();
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        }
        System.out.println("after try catch");
     }
     
    public static void func() {
        int[] arr = {1, 2, 3};
        System.out.println(arr[100]);
     }
     
    // 直接结果
    java.lang.ArrayIndexOutOfBoundsException: 100
        at demo02.Test.func(Test.java:18)
        at demo02.Test.main(Test.java:9)
     after try catch
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM 处理, 程序就会异常终止(和我们最开始未使用 try catch 时是一样的)。

    public static void main(String[] args) {
        func();
        System.out.println("after try catch");
     }
     
    public static void func() {
        int[] arr = {1, 2, 3};
        System.out.println(arr[100]);
     }
     
    // 执行结果
    /**Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
        at demo02.Test.func(Test.java:14) 先解决第14行的异常
        at demo02.Test.main(Test.java:8)
        */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到, 程序已经异常终止了, 没有执行到 System.out.println(“after try catch”); 这一行。

    【异常处理流程总结】

    1.程序先执行 try 中的代码
    2.如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配. 
    3.如果找到匹配的异常类型, 就会执行 catch 中的代码
    4.如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者. 
    5.无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行). 
    6.如果上层调用者也没有处理的了异常, 就继续向上传递. 
    7.一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止. 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5.自定义异常类

    Java 中虽然已经内置了丰富的异常类, 但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构.
    例如, 我们实现一个用户登陆功能.

    public class LogIn {
        private String userName="admin";
        private String passWord="123456";
        public void LogInOf(String userName,String passWord){
            if(!userName.equals(this.userName)){
            }
            if(!passWord.equals(this.passWord)){
            }
            System.out.println("登陆成功!");
        }
    
        public static void main(String[] args) {
            LogIn logIn=new LogIn();
            Scanner scan=new Scanner(System.in);
            logIn.LogInOf(scan.nextLine(),scan.nextLine());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    此时我们在处理用户名密码错误的时候可能就需要抛出两种异常. 我们可以基于已有的异常类进行扩展(继承), 创建和我们业务相关的异常类.
    具体方式:
    1.自定义异常类,然后继承自Exception或者RunTimeException
    2.实现一个带有String类型参数的构造方法,参数含义:出现异常的原因
    (1)UserNameException异常

    public class UserNameException extends RuntimeException{//也可继承Exception(非受查异常)
        public UserNameException(){
    
        }
        public UserNameException(String args){
            super(args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (2)PassWorldException异常

    public class PassWorldException extends RuntimeException{//也可继承Exception(非受查异常)
        public PassWorldException(){
    
        }
        public PassWorldException(String args){
            super(args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时我们的 login 代码可以改成

    public class LogIn {
        private String userName="admin";
        private String passWord="123456";
        public void LogInOf(String userName,String passWord){
            if(!userName.equals(this.userName)){
                throw new UserNameException("用户名异常");
            }
            if(!passWord.equals(this.passWord)){
                throw new PassWorldException("登录密码异常");
            }
            System.out.println("登陆成功!");
        }
    
        public static void main(String[] args) {
            LogIn logIn=new LogIn();
            Scanner scan=new Scanner(System.in);
            try{
                logIn.LogInOf(scan.nextLine(),scan.nextLine());
            }catch (UserNameException e){
                System.out.println("用户名输入错误");
                e.printStackTrace();
            }catch (PassWorldException e){
                System.out.println("密码输入错误");
                e.printStackTrace();
            }finally {
                scan.close();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    登陆成功运行结果
    在这里插入图片描述
    用户名和密码都错误运行结果
    在这里插入图片描述
    捕捉到用户名异常后直接退出
    注意事项
    自定义异常通常会继承自 Exception 或者 RuntimeException
    继承自 Exception 的异常默认是受查异常
    继承自 RuntimeException 的异常默认是非受查异常.

  • 相关阅读:
    docker容器的设置本地时间(/etc/localtime)和本地时区(/etc/timezone)
    ThreadLocal夺命11连问
    在页面使用富文本编译器
    leetcode - 823. Binary Trees With Factors
    table多行表头渲染时出现位置错乱问题
    php+vue+Elementui大学生心理健康测评网站
    Go基础——指针、结构体
    leetcode-676:实现一个魔法字典
    ReactNative 箭头函数=>
    service workers跟页面dom交互
  • 原文地址:https://blog.csdn.net/li_wj6/article/details/138122964