Java学习 day_15 Exception

异常

Java异常概述

异常,异常,即是程序运行出现了不正常的情况,报错,并导致程序终止

  • Java当中一切皆对象,当程序产生异常,JVM会把异常信息封装成一个对象(类)
    • 类中封装着问题的名称,产生的原因、描述等多个属性信息存在
    • 以及对这些信息进行操作的一系列方法(属性+行为)
    • 这些类通过继承层次构成了Java的异常体系
  • 学习异常之前,需要明确的一个重要点
    • 异常的类和对象中只是存了异常的信息
    • 包括异常的原因,异常的种类等等
    • 但是何时抛出异常,怎么处理异常,不是由异常对象决定的

Java程序运行时碰到了一系列的问题

  • 编译时期,必须要检查处理的异常,不检查不能通过编译。即便发现了问题,仍然可以解决
  • 运行期间,无法预料的问题,但是出现问题后,我们仍然能解决它
  • 运行期间,无法预料的问题,但是出现问题后,无法解决它,是一个严重的错误

我们一般根据问题能不能处理,也就是问题的严重程度,来区分异常和错误

  • Java程序运行时碰到了一系列的问题
    • 严重问题
      • 运行时,无法预料,且无法解决的错误
    • 一般问题
      • 编译时要检查处理的异常,出现问题,可以被解决
      • 运行时,无法预料,且能够被解决的问题

Java异常体系确实就是这样划分的

  • Throwable(祖先类)

    • Error

    • Exception

      • RuntimeException 运行时异常

        RuntimeException和它的所有子类都是运行时异常

      • 非RuntimeException 编译时异常

  • Throwable是Java一切错误和异常的父类,是继承层次中的祖先类

    • 表示可以由程序显式或者JVM抛出的问题
  • Error是严重问题,无法被解决

    • Error描述了Java运行时虚拟机内部错误和资源耗尽错误
    • 对于Error,程序自己是无能为力的,仅靠程序本身是无法恢复和和预防
    • 于是程序只能尽量安全得保存数据, 然后终止程序,并通知用户去解决
    • 常见的Error是栈溢出,或者堆溢出这些错误
  • Exception是一般问题,能够被解决

    • RuntimeException,指的是在程序运行期间,发生的一般问题,称之为运行时异常
      • 这种问题无法在编译时检查和预料,只有到程序运行后才能显现问题
      • 例如用null调用方法,数组使用错误的下标,错误的强制类型转换
      • RuntimeException可以写代码进行正常的处理,属于一般问题

    需要明确的是:

    运行时异常绝大多数都是因为编码问题所导致的

    也就是可以避免的异常,当程序抛出该异常后,最好能够重构代码,修正问题

    • 非RuntimeException,指的是在编译时期,就需要显式的检查并处理的异常,称之为编译时异常
      • 部分书籍也称其为,受检查的异常(Checked Exception)
      • 这种异常必须在编译期检查和处理
      • 例如打开一个文件夹(要考虑该文件夹存不存在)
      • 克隆一个对象(要检查该类是否实现Cloneable接口)
      • Exception的子类中,只要不是RuntimeException的子类,那必然是非RuntimeException

Error和Exception的异同

  • 两者都继承自Throwable类,共同构成了Java的异常体系
  • Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等
    • 对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防
    • 遇到这样的错误,只好尽量安全得保存数据, 然后终止程序,并通知用户去解决
    • Error一般都不由程序显式的抛出,而是由JVM抛出
  • Exception类表示程序可以处理的异常,可以捕获且可能恢复
    • 遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意地终止程序
    • Exception又分为RuntimeException(运行时异常)和非RuntimeException(编译时异常)
      • 运行时异常不受编译器检查,在程序运行期间发生,一旦发生,若不处理就会导致程序终止
        • 运行时异常包括所有RuntimeException的子类
      • 编译时异常受编译器检查,必须显式地处理该异常,才能够通过编译
        • 编译时异常包括所有Exception的子类中,非RuntimeException的子类的类

补充编译时异常和运行时异常区分

运行时异常:如NullPointerException、IndexOutOfBoundsException。这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,可以从逻辑角度出发去处理,尽可能避免这类异常的发生。
——————————————————————————————————————
编译时异常:这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。


异常的处理

Java程序的异常处理分为两种方式

  • 自己处理
  • 向上抛出
  • 某个方法发生了异常

    • 自己解决
    • 抛给该方法的调用者
  • 调用者(方法)拿到了异常

    • 自己解决
    • 抛给该方法的调用者
  • 这个过程可以将异常一直向方法的调用者抛出,但不可能是无限的

  • 最终main方法,程序的入口方法拿到了该异常,它可以选择

    • 自己解决
    • 继续抛出
  • 这个时候,能接收main方法抛出的异常的,就只有JVM

    • JVM必须自己处理该异常

Java默认的异常处理机制

如果我们在程序中,不写任何和异常处理相关的代码,Java程序仍然能够对异常进行处理

  • 如果错误产生在main方法中
    • 当我们的代码执行到错误行数之前,代码是正常执行的
    • 当我们的代码执行到错误行数时,JVM会终止程序的执行,抛出一个该异常信息封装成的对象
    • 将该对象中的异常信息,打印到控制台上,告诉程序员发生了什么问题
    • 发生错误之后的语句,都不执行了
  • 如果错误产生在main方法当中的另一个方法中
    • 当程序执行到该方法的错误行数时,JVM会终止程序的执行
      • 向上给方法的调用者抛出一个该异常信息封装成的对象
    • 一直向上抛出,直到抛给main方法,main方法最终抛给JVM
    • 发生异常之前的语句正常执行,但是之后的语句都不执行了

注意事项:

​ 这种默认处理机制只是针对运行时异常的,对于编译时异常,程序员必须在编译时手动处理它


自定义(手动)异常处理

显然,Java默认的异常处理机制,总会导致程序终止执行,这不能够满足我们的需求

我们需要手动显式的来处理异常,以达到自己的目的

捕获异常,自己处理

Java提供了结构try…catch用来捕获并处理异常

单分支try…catch
  • 顾名思义,其语法为

    • try {
       //可能出现异常的,正常的代码逻辑
      } catch(要捕捉的异常对象) {
       //每一个catch分支对应一个异常处理器
       //在catch分支中处理具体类型的代码异常
      }
      
  • 如果try代码块中发生了异常,那么JVM就会收集这个异常的信息,封装成对象

  • catch语句中需要填入一个对象引用作为匹配,而不是使用类型来匹配

    • 这个引用接收(指向)JVM封装的对象
  • try和catch代码块都是代码块,它们里面定义的变量都是局部变量

  • catch语句中可以填入多个对象来作为匹配

    • catch(要捕捉的异常类型1 | 要捕捉的异常对类型2 | 要捕捉的异常类型3 对象名...){
          
      }
      
    • 注意:无论你能够匹配多少种异常类型,始终都只有一个异常对象被接收,对象名只写一个

  • catch当中填入要捕捉的异常类型,如果能够匹配这个对象,那么就会执行catch代码块中的代码

    • 怎么算匹配?
    • 可以就是这个类的对象
    • 也可以是这个类的子类对象
    • catch代码块中语句可以对该异常进行处理
  • 一旦匹配成功,catch中写的异常对象就会接收JVM抛出的异常对象

  • 匹配失败,那么程序依然会自动向上抛出异常,直到JVM默认处理

  • try代码块中发生了异常,在catch代码块中被处理了,那么程序仍然能够继续执行

  • 在这里插入图片描述

单分支try…catch使用注意事项

  • try…catch会显著的影响代码结构,严重影响代码可读性

    • 所以应该把尽量少的代码放入try中,最好是产生异常的那一行代码
  • 如果catch不能匹配异常对象,那么不会执行catch代码块中的内容

  • 只有当try代码块中产生了异常,catch才有机会执行,没有异常不执行,不匹配也不执行

  • try代码块中某个位置产生了异常,那么try中的异常后面的代码就不继续执行了

    • 也就是说try当中要么不产生异常,要么只会产生一个异常

小练习

  • 看代码说执行结果
try {
    System.out.println("before");
    System.out.println(10/0);
    System.out.println("after");
}
catch (ArithmeticException e){
    System.out.println("发生了异常");
}
System.out.println("try..catch之后");
  • 看代码说执行结果
try {
    System.out.println("before");  // 执行
    System.out.println(10/0);   // 执行,异常,到catch
    System.out.println("after"); // 不执行
}
catch (RuntimeException e){
    System.out.println("发生了异常"); //执行
}
System.out.println("try..catch之后"); // 执行
  • 看代码说执行结果
try {
    System.out.println("before");// 执行
    System.out.println(10/0); // 执行,产生异常
    System.out.println("after");
}
catch (NullPointerException e){
    System.out.println("发生了异常"); // catch异常不匹配,catch不执行
}
System.out.println("try..catch之后"); // 不执行
  • 看代码说执行结果
try {
    System.out.println("before"); // 执行
    System.out.println(10/10); // 执行
    System.out.println("after"); //执行
}
catch (NullPointerException e){
    System.out.println("发生了异常"); //不执行
}
System.out.println("try..catch之后"); //执行
获取捕获异常对象中的异常信息

既然能捕获该异常对象,那么获取其中的异常信息也是势在必行的

使用捕获的异常对象,能够调用的常用API有(异常对象进行调用,比如catch后括号中定义的对象调用)

//获取异常信息,返回字符串。打印的实际上是异常产生的原因 
(不使用)String getMessage()
//获取异常类名和异常信息,返回字符串。 打印的是异常产生的原因和所在类
(不使用)String toString()
//获取异常类名和异常信息,以及异常出现在程序中的位置
(推荐使用)void printStackTrace()
//使用IO流,将异常内容保存在日志文件中,以便查阅,早已过时,了解即可
(过时的日志处理方式)printStackTrace(PrintStream s) 


// 给个例子,但是具体有啥用,有待我研究
 ArithmeticException ae = new ArithmeticException("心情不好,想抛出异常!");
 ae.printStackTrace();

多分支try…catch

如果代码只会发生一个异常,那么单分支的try…catch就够用了,那么如果发生多个异常呢?

  • 语法为

    • try {
       //可能出现异常的,正常的代码逻辑
      } catch(要捕捉的异常对象1) {
       //每一个catch分支对应一个异常处理器
       //在catch分支中处理具体类型的代码异常
      }catch(要捕捉的异常对象2) {
       //每一个catch分支对应一个异常处理器
       //在catch分支中处理具体类型的代码异常
      }
      ...
      
  • 该格式和单分支try…catch并无实质不同,只是多个几个catch分支而已

  • 多分支try…catch的匹配流程

    • 根据实际的异常对象的类型,和catch中声明的异常类型,从上到下一次做类型匹配
    • 一旦通过类型匹配,发现实际异常对象的类型和catch中的异常对象类型匹配
      • 就把该异常对象交给这个catch分支进行处理(异常处理器)
    • 没有相匹配catch代码块的异常,那么程序依然会自动向上抛出异常,直到JVM默认处理

多分支try…catch使用注意事项

  • 多分支的异常处理的执行,有点类似于多分支if-else的执行,一次匹配,只会执行多个catch分支中的一个

  • 如果多个catch中处理的是毫无关系的异常,那么catch的顺序并不需要特别注意

  • 如果多个catch中处理的异常有父子关系,那么就必须要注意了

    • 如果父类异常写在了上面,那么子类异常的catch分支就永远没有机会执行了,并且会报错
  • 所以,应该把具体子类放在catch分支的上面作类型匹配,父类放在后面作兜底

  • 两种运行时异常

    • NullPointerException: 运行时异常,空指针异常,用一个等于null的引用调用了方法和成员变量
      IndexOutOfBoundsException:运行时异常,数组下标越界异常。调用了超过数组最大下标的位置
      

牛刀小试

  • 需求:单独处理除0异常,但是空指针和下标越界异常一起处理
  try {
            //System.out.println(10 / 0);
            int[] arr = null;
            System.out.println(arr.length);
        } catch (ArithmeticException ae) {
            ae.printStackTrace();
       } 
       catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
            //'catch' branch identical to 'ArithmeticException' branch
            System.out.println("同时处理空指针和数组下标越界");
            e.printStackTrace();
        }

抛出异常,上层处理

程序总会有在异常发生处不想处理该异常的情况,Java提供了向上层抛出异常的解决方案

throws关键字

throws关键字表示在方法上抛出异常

  • 在方法声明时使用,声明该方法可能抛出的异常类型

语法:

方法名(形参列表) throws 异常列表{
}

注意事项:

  • 运行时异常会自动向上抛出,不用我们手动throws

    • 我们只需要手动throws编译时异常
  • 如果方法抛出一个编译时异常,可以在语法层面,强制要求方法调用者处理该异常

  • 异常列表可以是多个异常类,但是注意用逗号隔开

  • 列表中出现的异常如果有父子关系,那么编译器只会强制要求处理父类

    • 所以尽量抛出同级别的异常
private static void testThrowsMethod1() throws ParseException, CloneNotSupportedException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date date1 = null;
        date1 = simpleDateFormat.parse("2020/10/17 9:55:02");
        System.out.println(date1.getTime());

        Demo d = new Demo();
        d.clone();
}

throws的作用:在方法声明中提前声明可能抛出的异常,如果该异常是编译时异常,那么方法的调用处就必须

显式处理该异常。也就说可以提前警告方法调用者,让它做好应对异常的准备

方法覆盖中的异常列表匹配问题

首先,异常说明属于方法声明的一部分,紧跟在形式参数列表之后

方法的声明中加了throws关键字表示所有要抛出的潜在异常类型后

方法在重写的时候也会发生一些变化

  • 总体上的原则是:子类中的覆盖方法,不能比父类中的方法抛出更多异常(单指编译时异常)
    • 因为Java中存在多态现象,当用父类引用调用方法时, 如果允许子类重写后有更多的异常,那么就没有办法处理
    • 运行时异常显式抛出对程序并无影响,那么子类重写方法可以多抛出运行时异常
  • 如果子父类方法,完全抛出相同的异常,允许进行方法的重写
  • 如果父类方法没有抛出异常,子类重写方法,要么也不抛出异常
    • 要么就只能抛出运行时异常(本身就是自动的,不算多)
  • 如果父类方法抛了异常,那么
    • 子类重写方法可以选择完全不抛出异常
    • 如果父类方法抛出的是RuntimeException,那么子类重写方法也只能抛出RuntimeException
      • 种类不限制,允许类型不同
      • 父类方法抛出一个RuntimeException子类,子类方法重写可以是RuntimeException
    • 如果父类方法抛出的是编译时异常,那么子类重写方法
      • 可以抛出相同的编译时异常,但不能抛出不同的编译时异常
      • 抛出所有运行时异常
      • 不可以抛出Exception
    • 如果父类方法直接抛出Exception
      • 那么子类重写方法就可以抛出任何异常了

我们其实并不需要特别记忆这些规则,实际开发中,我们并不是像老师上课一样,需要一点不能出错。我们可以不断的尝试,然后最终提交出一份正确的代码。这样,我们仍然是一名优秀的Java开发工程师。如果面试中被问到,建议直接说子类中的覆盖方法,不能比父类中的方法抛出更多异常即可。

  • 建议在开发中,子父类重写方法拥有一致的抛出异常列表
  • 避免自找麻烦

throw

在很多时候,我们不满于在方法声明中声明要抛出的异常的类型,我们想要直接在方法内部抛出异常

常见用途,代替return,在不太方便写返回值的方法中

概述:

​ 1.在方法体中使用,主动在方法中抛出异常

​ 2.每次只能抛出确定的某一个异常对象

表示明确的在方法的这个位置,这行代码,抛出一个异常对象,程序执行到这一行后,相当于这一行产生了异常,如果不做处理,就向上抛出,然后这一行后面的代码就不能执行了


基本语法:

  throw 异常对象;

注意:

  • 每次只能抛出一个异常对象
  • 一旦程序运行到该throw代码,必然会抛出一个异常对象
  • 在方法中抛出一个异常,相当于使用了return关键字
    • 方法立刻结束,后面也不能有其它代码了
    • throw必须位于方法的最后一行
  • 每个异常类的构造方法都可以显式得传入一个字符串,表示异常信息(原因)的说明
  • 如果在方法中显式地抛了一个编译时异常
    • 那么会和产生编译时异常的代码一样,需要显式处理
    • 最好和throws一起使用(如果try…catch就没有意义)
  • 一旦主动使用throw关键字,就代表在当前方法中,必然不会处理该异常,此时直接抛给方法调用者去处理
  • 举例:
    • 结合成员变量的封装和set方法,判断输入的参数是否合法(IllegalArgumentException)
    • 空接口(Cloneable)
private static void testThrowCheckableDemo() throws CloneNotSupportedException {

    throw new CloneNotSupportedException("发生了禁止克隆异常");

}

private static void testThrowRuntimeDemo() throws NullPointerException{

    throw new NullPointerException("发生了空指针异常");

}

牛刀小试

  • 编译期异常和运行期异常的区别?
    • 需要在编译时期显式处理: 编译时异常
    • 不需要在编译时期处理的: 运行时异常
throws和throw的区别
  • throws
1.用在方法声明后面,跟的是异常类名

2.可以跟多个异常类名,用逗号隔开

3.表示抛出异常,由该方法的调用者来处理

4.throws表示出现异常的一种可能性,并不一定会发生这些异常
  • throw
1.用在方法体内,跟的是异常对象名

2.只能抛出一个异常对象

3.表示抛出异常,可以由方法体内的语句处理(多此一举) 最常见的是结合throws抛给调用者去处理

4.throw则是抛出了异常,执行throw则一定抛出了某种异常
到底是该try…catch还是该抛出

总结一下,目前为止,我们所学习过的异常的处理策略主要有两种:

  • 捕获并处理 try -catch

  • 向上抛出

    • 运行时异常会自动上抛,直到抛给JVM
    • 编译时异常需要用throws关键字向上抛出
  • 那么究竟,在遇到异常时我们该如何选择处理策略呢?

  • 原则:

 对于运行时异常,我们不应该写出产生这种异常的代码,应该在代码的测试阶段修正代码。
 对于编译时异常,功能内部能够处理的就处理,如果不能够或者没有必要处理,就抛出。

为什么不能子类抛出更多编译时异常?

因为方法有多态现象,如果允许,可能会出现父类指向的显式没有异常,但是实际上子类出现异常

finally

finally的特点

  • 无论try中是否发生异常,都会执行
  • try-catch代码中有return也不能阻止它
  • 特殊情况:在执行到finally之前jvm退出了
    • System.exit(0)

finally的作用

  • 利用finally代码块无论如何都要执行的特点
  • 所以经常用于释放资源,在IO流操作和数据库操作、网络编程等需要操作额外资源的场景中十分常见

例如

private static void method() {
    try {
        System.out.println(10/0);
    }
    catch (ArithmeticException e){
        e.printStackTrace();
        System.exit(0);
    }
    finally {
        System.out.println("finally");
    }

一些奇思妙想

  • try代码块如果有return
    • 程序会先执行完finally代码块,回过头执行try中的return
  • catch代码块中如果有return,并且catch正常捕获异常执行
    • 程序会先执行完finally代码块后,再回去执行catch中return,从catch代码块中结束方法
  • finally代码中有return
    • 不会影响finally代码块执行
  • 如果finally和catch中都有return
    • 程序会直接从finally代码块中的return结束方法
  • 如果try中的异常不能正常捕获,但是finally中有return
    • 注意此时程序会跳过这个异常,不会抛出异常给JVM报错
  • try中的return语句return的是一个方法,但是这个方法又产生了异常
    • 自己测试一下吧哈哈

try…catch变形

  • try…catch…finally
  • try…catch…
  • try…catch…catch…
  • try…catch…catch…finally
    • 以上都是常见形式
  • try…finally
    • 该形式在不需要捕获异常,但是希望使用finally代码块的场景中使用

注意事项

  • 普遍来说,finally需要放在一个catch代码块后面,但这不是必须的,try…finally的结构也是合法的
  • 如果某处代码产生异常,但是我不希望在这里立刻处理异常,而是希望抛给调用者
    • 但是,我又希望不管有没有产生异常,都要做一系列操作
    • 那么选择try…finally结构是可行的,可以实现需求
final,finally和finalize的区别
  • final关键字,最终的,最后的。可以修饰类 成员变量 成员方法
    • 修饰类,该类不能被继承
    • 修饰变量表示一个常量
    • 修饰方法表示无法重写的方法
  • finally代码块,和try…catch一起使用,具有必然执行的特点
    • 异常处理体系当中,用于资源释放
  • finalize方法,Object()类中的成员方法,用于做对象销毁的善后工作,释放额外的系统资源
    • 由于GC的自动回收不确定性,该方法实际无效

自定义异常

定义一个类,然后让它

自定义异常是程序员自己写的异常,我们具体知道异常在什么地方抛出,这样可以有针对性地找出异常产生原因

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值