• 【Java进阶篇】第四章 异常处理


    在这里插入图片描述

    程序执行过程中发生了不正常的情况,JVM将异常信息打印到控制台,从而方便调试和增强程序的健壮性。

    1、异常

    异常在Java中以类的形式存在,如NullPointerException,每一个异常类都可以创建异常对象。

    NumberFormatException nfe = new NumberFormatException("数字格式化异常");
    
    NullPointerException npe = new NullPointerException("空指针异常");
    
    • 1
    • 2
    • 3

    举个例子:

    int a = 10;
    int b = 0;
    int c = a/b;
    //当JVM执行到这个地方时,会new异常对象,即new ArithmeticException("/by zero");
    //并且JVM将new的异常对象抛出,打印输出信息到控制台
    
    • 1
    • 2
    • 3
    • 4
    • 5

    类比于生活:

    火灾是一种异常(一种抽象出来的类),而异常对象则是xxx地方发生火灾了。

    2、类Throwable

    父类 java.lang.Object<-------java.lang.Throwable类------>子类Error和Exception

    插播:
    UML,即Unified Modeling Language,统一建模语言。
    
    用来描述类个类之间的关系,程序执行的流程,对象的状态等。
    
    对应的工具有:Rational Rose,startUML
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Throwable类

    3、运行时异常和编译时异常

    定义

    • 所有Exception类的直接子类,称编译时异常(并不是字面意思上的编译阶段发生的)该类异常,必须在编写程序时,预先对这种异常进行处理,不处理,则编译时抛错
    • RuntimeException及其所有的子类,称为运行时异常,该类异常在编写程序时,可处理,也可不处理

    在这里插入图片描述
    异常的发生就是new对象,而只有运行阶段可以new对象,故运行时异常和编译时异常都是发生在运行阶段。

    两种异常的区别

    • 编译时异常发生的概率较高,又称受检异常、受控异常。类比生活中的:出门前看到外面正在下大雨,可预料,不打伞出门大概率生病,生病这个”异常“发生的概率很大,所以出门前要做”处理“拿伞。
    • 运行时异常发生的概率比较低,又称非受检异常、非受控异常。类比生活中:走在大街上,被天上掉下来的陨石砸中了,概率极低,没必要提前做出预处理。若处理,你活着就很累,相对应的,代码就很本末倒置。

    4、异常的处理

    处理思路1:

    在调用者方法声明的位置上,使用throws关键字,抛给上一级,即谁调用,抛给谁

    public class Exception1 {
        public static void main(String[] args) throws ClassNotFoundException {
            //这里main方法调用doSome方法,必须对这个异常进行预处理
            doSome();
        }
    
        //doSome方法的声明位置有throws ClassNotFoundException
        //这表示这个方法执过程中很有可能会出现ClassNotFoundException异常
        //ClassNotFoundException异常的直接父类是Exception,即它是编译时异常
        public static void doSome() throws ClassNotFoundException{
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    😉
    在Java中,异常发生之后如果一直上抛,最终抛给main方法,main方法继续向上抛到JVM,到JVM,程序会被终止执行。

    处理思路2:

    使用try……catch语句进行异常的捕捉

    public class Exception1 {
        public static void main(String[] args) {
            try {
                doSome();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        public static void doSome() throws ClassNotFoundException{
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    try{        //尝试
    	m1();
    }catch(FileNotFoundException e){   //e是一个引用,保存了new出来的那个异常对象的内存地址
    
    	System.out.println("文件不存在");   //捕捉到异常以后执行的语句
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    对于运行时异常,如ArithmeticException,可以处理,也可以不处理

    try{
    	System.out.println(100/0);
    catch(ArithmeticException e){
    	System.out.println("算术异常");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    总结:

    • throws上抛类似于推卸责任,继续吧异常传递给调用者,让调用者来决策怎么处理(继续抛还是捕捉)
    • try…catch捕捉等于把异常拦下解决了,调用者是不知道的
      异常处理图示

    几点注意事项:

    在这里插入图片描述

    • 在方法的定义中,+throws 异常类名,该异常类名可以是该异常类的父类,也可以是异常类名,异常类名1,异常类名2,异常类名3
    • 不建议在main方法上使用throws,因为异常若真的发生了,一定抛给JVM,则JVM会终止程序执行。而异常机制的作用和初衷就是增强程序的健壮性(即要做到异常发生了也不影响程序的执行)
    • main方法中用try…catch ,别上抛了。

    5、异常导致某些代码不能被执行

    异常导致某些代码块不能被执行

    总结:

    • 只要异常没有被捕捉,即采用了上报的方式,则当某行代码出现异常的时候,此方法结束,方法中的后续代码不会被执行,相当于return。再往下分析,就只看其的上抛(调用者)就好。
    • try语句块中,某一行出现异常后,该行后面的代码不会执行,catch捕捉异常后,catch后的代码继续执行。

    6、try…catch总结

    😉 catch后,写确切的类型、写其父类型、父父类型也可以

    try{
    	FileInputStream fis = new FileInputStream("D:\\not_exist.java");
    //这里写的是FileNotFoundException的父类IO Exception
    }catch(IO Exception e){
    	System.out.println("文件不存在");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    😉 catch可以写多个,当这些异常父类一样时,直接写一个他们的父类也行,但建议精确catch,方便调试

    try{
    	...
    //出现异常时,从上往下匹配catch
    }catch(xx e){
    	...
    }catch(xxx e){
    	...
    }catch(xxxx e){
    ...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    😉 catch写多个的时候,从上到下,必须遵循从小到大,若父类异常在上,则catch匹配时,父类下的精确子类一定轮不上执行,会报错。


    😉 JDK8中,允许使用或"|"来连接多个异常

    try{
    	...
    }catch(FileNotFoundException|ArithmeticException|NullPointerException e){
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上抛和捕捉如何选择?
    ------

    如果希望调用者去处理,则用throws抛给调用者,反之,自己处理则try…catch

    7、异常对象的常用方法

    Exception exception = new Exception();
    
    • 1

    1)getMessage()方法

    //作用:获取异常的简单描述信息
    String msg = exception.getMessage();
    
    • 1
    • 2

    2)printStackTrace()方法

    //作用:打印异常追踪的堆栈信息
    exception.printStackTrace();
    
    • 1
    • 2

    举例:

    public class Exception2 {
        public static void main (String[] args){
        	//异常的发生即new异常对象,那这行代码会导致程序终止运行吗?
        	//不会,这里只是new了一个异常对象,但并没有将异常对象throws抛出
        	//因此JVM会认为这是一个普通的Java对象
            NullPointerException e = new NullPointerException("发生空指针异常");
            
            String msg = e.getMessage();
            //getMessage方法的输出,其实是用有参构造方法传入的那个String
            System.out.println(msg);
            e.printStackTrace();
            System.out.println("Hello World!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:

    多次运行可以看到,Hello World一会儿在堆栈信息之上,有时在堆栈信息之下,这是因为Java打印异常堆栈信息追踪信息的时候,采用的是异步线程的方式。

    运行结果

    Tip:

    printStackTrace打印堆栈信息的结果也为红色,不是编译器出错了,如果是出错了,那下面的代码是不会执行的。错误分析时,从上往下看异常,下面的异常通常是上面的导致的。此外注意看包名,sun公司包后面的信息可以适当跳过。

    8、try…catch和finally

    • 😉 在finally子句中的代码是最后执行的,并且一定会执行,即使try语句块中的代码出现了异常
    • 😉 finally子句必须和try一起出现
    • 😉 通常在finally语句块中完成资源的释放/关闭,以保障资源一定被关闭
      在这里插入图片描述
    • 😉 也可无catch,直接try+finally
      在这里插入图片描述
    • 😉 退出JVM时,finally语句不执行
    将上面try语句块中的return;改为:
    
    System.exit(0);
    //再运行,则只输出try,finally中的语句不再执行
    
    • 1
    • 2
    • 3
    • 4

    整理一个finally的坑,或许面试用得上:

    public class Exception2 {
        public static void main (String[] args){
            int result = m();
            System.out.println(result);   //100
        }
        public static int m(){
            int i = 100;
            try{
                return i;
            }finally{
                i++;
            }
        }
    }
    -----
    //按照刚学的执行顺序,应该是先i++,再return,但这里是特殊
    //使用反编译工具可以看到m()方法其实是这样:
    public static int m(){
    	int i = 100;
    	int j = i;
    	i++;
    	return j;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    9、final、finally和finalize的整理区分

    • final是一个关键字,表示最终的,其修饰的类无法继承、修饰的方法无法覆盖、修饰的变量不能重新赋值
    • finally是一个关键字,和try一起出现,finally语句块中的代码必须执行(除非中途退出JVM)
    • finalize是Object类中的方法名(即标识符),该方法由GC机制调用

    10、如何自定义异常

    SUN提供的JDK内置异常不够用,实际的开发中有的异常和业务挂钩,这种异常需要我们自己定义,自定义异常的步骤是:

    • 编写一个类继承Exception(编译时异常)或者继承RuntimeException(运行时异常)
    • 提供两个构造方法,一个无参数的,一个带有String参数的
    public class MyException extends Exception{
    	public MyException(){
    	}
    	public MyException(String s){
    		super(s);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    11、异常在实际开发中的作用

    以下以数组模拟压栈弹栈为改良例子:

    //之前的栈满是通过return+println来表达异常
    public void push(Object obj){
            if(this.index >= this.elements.length-1){
                System.out.println("压栈失败,栈满");
                return;
            }
            this.index++;
            this.elements[index] = obj;
            System.out.println("压栈"+obj+"成功,栈帧指向"+ index);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    栈满是一种异常,我们用异常机制来改良:

    /**
     * 改良后的压栈方法
     */
    public void push2(Object obj) throws MyStackOperationException {
        if(index >= elements.length-1){
        	//创建异常对象,并将其抛出
            MyStackOperationException e = new MyStackOperationException("栈满");
            throw e;
            //抛出异常后,因为这个自定义异常我写的是编译时异常,故要预处理
            //这里选择throws,我定义异常的目的就是把栈满的异常信息传递出去
            //自己定义再自己捕捉,一切都没意义了,所以在方法名定义的末尾加上throws
        }
        this.index++;
        this.elements[index] = obj;
        System.out.println("压栈"+obj+"成功,栈帧指向"+ index);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    throw new MyStackOperationException("栈满");
    
    用这种抛异常来代替return + println
    
    • 1
    • 2
    • 3

    注意throw和throws的区别:

    • throws:在方法声明的位置上使用,表示上报异常信息给调用者
    • throw:手动抛出异常

    12、异常与方法覆盖

    覆盖那一章中提到:重写之后的方法,不能比重写之前的方法抛出更多或者更宽泛的异常,可以相等或者更少

    class Animal{
        public void doSome(){
            
        }
        public void doOther() throws Exception{
            
        }
    }
    class Cat extends Animal{
        //抛的更多了,error
        public void doSome throws Exception{
            
        }
        //抛出更少,√
        public void doOther(){
            
        }
        //抛出一样的,√
        public void doOther2() throws Exception{
            
        }
        //抛出范围更小的,√
        public void doOther() throws NullPointerException{
            
        }
        //抛出运行时异常,√
        public void doOther() throws RuntimeException{
            
        }
    }
    
    • 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
    • 30
  • 相关阅读:
    vue集成海康h5player实现播放
    让AI成为你的编程助手——DevChat AI插件在VSCode中的应用
    萌新看过来,你还学不懂VScode插件吗?
    数据库去除重复数据(id除外)
    数据接口工程对接BI可视化大屏(一)
    深度学习:到底怎么理解embedding
    大数据Doris(二十四):数据导入(Stream Load)介绍
    编程语言学习杂谈
    leetcode刷题 (9.1) 动态规划
    策略验证_卖出口诀_长箭射天股价落地
  • 原文地址:https://blog.csdn.net/llg___/article/details/127931571