Java异常超详细解析,异常捕获和匹配,异常分类,finally

前言❤️❤️

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
在日常写代码时,异常是我们几乎每天都会遇到的东西,例如数组下标越界,空指针异常…
Java中的异常规则还是比较重要的,为什么有的异常编译器强制要求处理,有的异常代码却可以直接编译运行?try‑catch 到底什么时候该写、什么时候不该写?finally 一定执行吗?多个 catch 的匹配规则是什么?
这篇博客就是详细的解析这些知识点🐵
请添加图片描述
🎇个人主页:洋不写bug的博客
🎇所属专栏:Java学习之旅,从入门到进阶
🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~

1,Java异常简介

异常,跟程序的错误处理是非常相关的,在写代码的时候,要未言胜,先言败

就拿王者荣耀来说,代码可能分为许多操作:登录,开始匹配,接受比赛,选择英雄,加载比赛地图…
那用户玩的时候,这些操作就一定都能顺利执行吗,会不会因为网卡了,或者服务器过载,造成中间的一个操作执行失败呢,当然是可能的。



因此,每一步操作都要充分考虑失败的情况,并且失败后要执行一定的操作帮助恢复正常
如果按照C语言的逻辑,那就是每次都判断下,如果失败的话,就执行相应的处理
在这里插入图片描述

这样写,正常流程的逻辑和错误流程的逻辑是混在一起的,代码就比较凌乱
Java中就通过异常处理,把正常的逻辑和错误处理的逻辑,分隔开了



Java中就通过try-catch语句,来达到这样的效果
将这些操作写在try语句中,当一个环节在执行过程中出错时,就会抛出异常,触发异常后,try中的代码就不会向下继续执行,就会跳转到下方的catch语句中执行相应的错误处理逻辑
在这里插入图片描述



一些异常可能我们平时写代码的时候经常遇到,例如算术异常
如下,报出的异常包括异常名和异常原因,下面时出现异常的位置

public class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        System.out.println(a/b);
    }
}

在这里插入图片描述



还有数组越界异常,会报出原因:下标100对于长度3来说越界了

public class Test {
    public static void main(String[] args) {
        int []arr = {1,2,3};
        System.out.println(arr[100]);
    }
}

在这里插入图片描述


再比如空指针异常,前后加个打印语句看一下,会发现出现异常后,程序就终止,不再运行了

public class Test {
    public static void main(String[] args) {
        String s = null;
        System.out.println("出现异常前");
        System.out.println(s.length());
        System.out.println("出现异常后");
    }
}

在这里插入图片描述



Java中异常的继承关系如下所示:

在这里插入图片描述

  • 左边的Error,是JVM内部使用的,我们写代码时不会主动抛出这样的异常,一般出现这样的错误后,代码无法进行进行程序必须终止,下面的StackOverflowError就是当递归结束条件那里出错了,无限递归,就会出现报出这个错误
  • 右面的Exception,这些异常是和代码直接相关的,学习异常主要学的就是这部分,我们try-catch捕获异常,也是捕获这里面的异常
  • 2,异常捕获

    下面这段代码,执行后并不会报错,结果是Infinity(无穷大),这个是double除法中的一个特殊情况

    public class Test {
        private static double divide(double a,double b){
            return a / b;
        }
    
        public static void main(String[] args) {
            System.out.println(divide(10,0));
        }
    }
    

    在这里插入图片描述



    我们就可以自己针对分母为0的情况抛出异常,限制这种做法
    创建一个异常类(必须是实现了Throwable接口的类的实例,不能随便创建一个对象)的对象,通过throw关键字触发抛出异常的行为
    这个对象可以传入异常原因,异常原因抛出异常后就会在控制台上显示
    如下所示

    public class Test {
        private static double divide(double a,double b){
            if(b == 0){
                throw new ArithmeticException("不能用0作为除数");
            }
            return a / b;
        }
    
        public static void main(String[] args) {
            System.out.println("抛出异常前");
            System.out.println(divide(10,0));
            System.out.println("抛出异常后");
        }
    }
    

    在这里插入图片描述



    如果抛出异常不管的话,那程序就会终止,如果我们用catch捕获一下异常,在catch方法里面执行错误处理逻辑
    catch异常后,就不会报出异常,程序也不会终止(这里只是演示下,很多异常是不建议我们去catch的,后面的异常分类会解析到)
    如下所示:

    package package11;
    
    public class Test {
        private static double divide(double a,double b){
            if(b == 0){
                throw new ArithmeticException("不能用0作为除数");
            }
            return a / b;
        }
    
        public static void main(String[] args) {
            System.out.println("抛出异常前");
            try{
                System.out.println(divide(10,0));
            }catch (ArithmeticException e){
                System.out.println("已捕获到异常");
                System.out.println("执行异常处理逻辑");
            }
            System.out.println("抛出异常后");
        }
    }
    
    

    在这里插入图片描述


    如果是连续的方法调用的话,那异常在下面的层中捕获到后,上面的层也就不再捕获了

    public class Test {
        private static double divide(double a,double b){
            if(b == 0){
                throw new ArithmeticException("不能用0作为除数");
            }
            return a / b;
        }
    
        private static void func1(){
            try {
                System.out.println(divide(10,0));
            }catch (ArithmeticException e){
                System.out.println("在func1中捕获异常");
            }
    
        }
    
        private static void func2(){
            try {
                func1();
            }catch (ArithmeticException e){
                System.out.println("在func2中捕获异常");
            }
    
        }
    
        public static void main(String[] args) {
            System.out.println("抛出异常前");
            try{
                func2();
            }catch (ArithmeticException e){
                System.out.println("在main方法中捕获异常");
            }
            System.out.println("抛出异常后");
        }
    }
    

    在这里插入图片描述

    如果在func1中没有捕获异常,异常就会到func2中,如果func2中也没有捕获异常,异常就会到main方法中,如果在main方法中,还是没有捕获异常,那程序就会终止

    异常抛出后,就会沿着调用栈,向上逐层处理,一般这个catch只写一层就够了



    当catch捕获到异常后,处理错误逻辑如下:

  • 相同方案再重试一次
  • 尝试使用备用方案
  • 通知程序员人工干预(打日志,发短信,打电话)


  • 在当前学习阶段,我们在catch中最常写的就是打印异常信息

    public class Test {
        private static double divide(double a,double b){
            if(b == 0){
                throw new ArithmeticException("不能用0作为除数");
            }
            return a / b;
        }
        public static void main(String[] args) {
            System.out.println("抛出异常前");
            try{
                System.out.println(divide(10,0));
            }catch (ArithmeticException e){
                e.printStackTrace();
            }
            System.out.println("抛出异常后");
        }
    }
    

    在这里插入图片描述

    如果没有捕获异常,强制终结程序时,也会打印出异常的信息,只是那是JVM打印的,这个是程序员自己打印的,打印这个就是能更好的知道异常的具体情况

    3,异常匹配

    一个try后面是可以写多个catch的(因为同一个try是可能会抛出不同的异常的),每个catch里面可以声明不同的异常类型
    当try抛出异常后,就会从上到下因此匹配每个catch的参数,哪个catch参数类型和抛出的异常类型匹配,就执行对应的catch逻辑
    一个异常一旦匹配到某个catch,就不会继续往下匹配其他的catch了

    public class Test {
        public static void main(String[] args) {
          try {
              int a = 10 / 0;
          }catch (ArithmeticException e){
              System.out.println("发生算术异常");
          }catch (NullPointerException e){
              System.out.println("发生空指针异常");
          }catch (IndexOutOfBoundsException e){
              System.out.println("发生数组越界异常");
          }
        }
    }
    

    在这里插入图片描述


    进行异常类型匹配的时候,不一定要是完全匹配,如果抛出的异常是声明异常类型的子类,也是能匹配到的

    下标越界异常的父类是RuntimeException(运行时异常),包括空指针异常,算术异常,它们的父类都是运行时异常
    在这里插入图片描述



    写成运行时异常,也是可以匹配到的,如下所示:

    public class Test {
        public static void main(String[] args) {
          try {
              int []arr = {1,2,3};
              System.out.println(arr[3]);
          }catch (ArithmeticException e){
              System.out.println("发生算术异常");
          }catch (NullPointerException e){
              System.out.println("发生空指针异常");
          }catch (RuntimeException e){
              System.out.println("运行时异常");
          }
        }
    }
    

    在这里插入图片描述



    当我们尝试空指针异常写到运行时异常下面时,就会报错,报错原因是空指针异常已经被捕获过了,如下图:
    因为异常是跟catch是从上往下匹配的,空指针异常一定会被上面的运行时异常捕获,因此下面写的这个是没有意义的
    在这里插入图片描述
    因此,如果在catch中同时写子类异常和父类异常的话,就要求把父类异常放在下面



    注:在try语句中,有能力抛出不同的异常,但是一次执行只能抛出一种异常

    下面这种就是根据我们的输入,try语句来抛出不同的异常

    import java.util.Scanner;
    
    public class Test {
        public static void main(String[] args) {
            try {
                Scanner scanner = new Scanner(System.in);
                int num = scanner.nextInt();
                if (num == 1) {
                    int a = 10 / 0;
                } else if (num == 2) {
                    String s = null;
                    System.out.println(s.length());
                } else {
                    int[] arr = {1, 2, 3};
                    System.out.println(arr[3]);
                }
            } catch (ArithmeticException e) {
                System.out.println("发生算术异常");
            } catch (NullPointerException e) {
                System.out.println("发生空指针异常");
            } catch (IndexOutOfBoundsException e) {
                System.out.println("发生数组越界异常");
            } catch (RuntimeException e) {
                System.out.println("运行时异常");
            }
        }
    }
    
    

    在这里插入图片描述



    但是像下面这样写是没办法同时抛出三个异常的,也即是一旦 try 里面抛出异常,try 块剩下的代码直接作废,不会继续跑

    public class Test {
        public static void main(String[] args) {
            try {
                int a = 10 / 0;
                String s = null;
                System.out.println(s.length());
                int[] arr = {1, 2, 3};
                System.out.println(arr[3]);
            } catch (ArithmeticException e) {
                System.out.println("发生算术异常");
            } catch (NullPointerException e) {
                System.out.println("发生空指针异常");
            } catch (IndexOutOfBoundsException e) {
                System.out.println("发生数组越界异常");
            } catch (RuntimeException e) {
                System.out.println("运行时异常");
            }
        }
    }
    

    在这里插入图片描述



    还有个万金油写法,这样所有的异常都能捕获到,如下:

    public class Test {
        public static void main(String[] args) {
            try {
                
            } catch (Exception e){
                System.out.println("捕获到异常");
            }
        }
    }
    

    但是不推荐这样写,因为引入这些异常类型,就是因为不同的不同的异常情况下有不同的处理逻辑,这样写,就相当于所有异常的处理逻辑都相同,这是不合适的(就像有的人各种病都吃抗生素,是不合适的😅)



    在catch最后再捕获个Exception,是为了防止遗漏一些异常,如下:

    public class Test {
        public static void main(String[] args) {
            try {
                int a = 10 / 0;
                String s = null;
            } catch (ArithmeticException e) {
                System.out.println("发生算术异常");
            } catch (NullPointerException e) {
                System.out.println("发生空指针异常");
            } catch (IndexOutOfBoundsException e) {
                System.out.println("发生数组越界异常");
            } catch (Exception e) {
                System.out.println("捕获到异常");
            }
        }
    }
    

    也可以在catch声明中,不同的异常用|隔开,就是同时捕获多种异常(我们一般也不这样搞,这里只是演示下),如下所示:

    try {
                
            } catch (ArithmeticException | IndexOutOfBoundsException | NullPointerException e) {
                System.out.println("同时捕获三种异常");
            }
    

    4,finally

    这里写个访问文件资源的代码(铁汁们大概看下即可,这是后续JavaEE的内容)

    访问完成文件资源后,要用inputStream.close()关闭一下文件资源,这一步是非常重要的,如果不关闭,后面就可能这个进程打不开新的文件,造成程序崩溃

    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    public class Test {
        public static void func(){
            try {
                InputStream inputStream = new FileInputStream("test.txt");
                //return语句
                //抛出异常
                inputStream.close(); //关闭文件资源
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    



    但是这里有个问题,这个关闭文件资源的语句放在try的末尾,一定会被执行到吗?
    并不一定,前面有个return语句,就会跳出这个方法;或者中间的逻辑报出异常,那这个关闭资源的语句都运行不到

    这时候我们就可以使用finally,finally是写到catch后面的,无论如何(抛出异常,return),finally中的逻辑都会运行

    这个关闭文件资源的操作也可能会报错,在finally语句中再try-catch一下
    另外,把inputStream的定义放在try语句上面,这样在finally中才能访问到

    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    public class Test {
        public static void func() {
            InputStream inputStream = null;
            try {
                inputStream = new FileInputStream("test.txt");
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    inputStream.close(); //关闭文件资源
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    



    这样就算前面写了return,那在退出方法前也会执行finally中的逻辑

    如果finally中也有return的话,就会覆盖掉前面语句的return(了解下即可),如下:

    public class Test {
        public static int func() {
            try {
                return 1;
            }catch (RuntimeException e){
    
            }finally {
                return 2;
            }
        }
    
        public static void main(String[] args) {
            System.out.println(func());
        }
    }
    

    在这里插入图片描述

    5, 异常分类

    Java中的异常分为两大类,受查异常和非受查异常

  • 受查异常:必须在代码中显示处理异常(try-catch,throws声明)
  • 非受查异常:可以不显示处理


  • 像前面打开关闭文件,抛出的异常就是IOException,这就是一种典型的受查异常(抛出这个异常就是文件可能找不到,让我们写下如果找不到文件采取什么措施),这种异常就必须进行显示处理,如下图
    在这里插入图片描述

    可以直接在func方法中进行try - catch

        public static void func() {
                try {
                    InputStream inputStream = new FileInputStream("test.txt");
                } catch (IOException e) {
                    System.out.println("文件打不开的处理逻辑");
                }
        }
    



    当然,也可以直接声明可能会抛出的异常
    拿示例来说,就是在func()方法中先不处理这个异常,直接声明出来,由后续这个方法的调用者来处理这个异常

    声明就是在方法名字后面用throws + 异常名,这样main方法调用func方法,就需要处理这个异常(如果在func方法中直接try-catch了,那就说明已经处理过异常了,main方法调用func就不需要处理了)
    在这里插入图片描述

    那在main方法中,同样也有两个选择:
    一种就是try-catch,在catch中写下处理逻辑

    public static void main(String[] args) {
            try {
                func();
            } catch (FileNotFoundException e) {
                System.out.println("异常处理逻辑");
            }
        }
    



    那另一种就是直接在main方法中声明这个异常,表示不考虑这个异常了,也就是认为文件一定能打开,或者这个异常导致程序终止问题不大

     public static void main(String[] args) throws FileNotFoundException {
            func();
        }
    



    那像前面聊的那些数组越界、空指针、算术异常
    这些都属于RunTimeException,就是非受查异常,我们不try-catch或者声明,程序是可以编译通过的(写出来没有爆红),这些就是非受查异常

    前面只是学习使用,正常情况下,很少会去主动捕获非受查异常,因为像空指针,数组越界,出现这些异常说明代码中有bug,那就需要修改代码了,这些问题越早暴露出来越好,捕获异常反而影响纠错代码

    Error(StackOverflowError)和 RuntimeException(空指针,数组越界,算术异常)是非受查异常,IOException是受查异常

  • 受查异常,一般都是对应着比较严重的问题,需要程序员一定重视的,此时通过受查异常显式的让程序员注意到这样的问题,通过合适的方式来处理
  • 非受查异常本质是代码 bug。大部分情况下就是让程序直接崩溃即可,程序崩溃后让程序员开始改bug
  • 6,自定义异常

    在标准库中虽然提供了异常类,但是还是有一些情况,标准库中的类是无法满足的,就需要我们自己创建一个异常类,如果想要这个异常类是受查异常,就继承Exception,想要这个类是非受查异常,就继承RunTimeException

    这里写个非受查异常,继承RunTimeException,要写个带参数的构造方法,抛出异常时就可以显示异常原因

    public class MyException extends  RuntimeException{
        public MyException(String message) {
            super(message);
        }
    }
    



    这里因为是非受查异常,在main方法中直接抛出,编译器也不会报错

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


    如果把MyException改为继承Exception,那编译器就会报错,就需要声明或者try-catch一下
    在这里插入图片描述



    用户登录时,可以写个用户名异常和密码异常(这里只是简单演示下,实际抛出异常后还会有处理逻辑),如下所示:

    public class UsernameError extends Exception{
    
        public UsernameError(String message) {
            super(message);
        }
    }
    
    public class PasswordError extends Exception{
        public PasswordError(String message) {
            super(message);
        }
    }
    
    import java.util.Scanner;
    
    public class Test {
        public static void login() throws UsernameError, PasswordError {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入用户名");
            String username = scanner.next();
            System.out.println("请输入密码");
            String password = scanner.next();
            if(!username.equals("admin")){
                throw new UsernameError("用户名错误");
            }
            if(!password.equals("123456")){
                throw new PasswordError("密码错误");
            }
        }
    
        public static void main(String[] args) throws UsernameError, PasswordError {
            login();
        }
    }
    

    总结

    在项目中,一般受查异常我们就是直接try-catch,非受查异常就让程序崩溃,然后去改bug,在JavaSE阶段简单学习下即可,具体情况的做法(比如说catch中要写什么错误处理逻辑)可以后期在项目中结合具体事例去学习🐵

    以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值