:::info Java异常是Java提供的一种识别及响应错误的一致性机制,java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。 :::

异常的层次结构

  1. 异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的 错误条件。当条件生成时,错误将引发异常。
  2. Java异常类层次结构图:1655694656(1).png

    Throwable

  3. Throwable 是 Java 语言中所有错误与异常的超类。

  4. Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
  5. Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。

    Error(错误)

  6. Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。

  7. 此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。
  8. 这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!

    Exception(异常)

    :::info 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。 :::

    运行时异常

  9. 都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

  10. 运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过

    非运行时异常 (编译异常)

  11. 是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

    异常可查分类

    可查异常

    :::info checked exceptions 编译器要求必须处置的异常 :::

  12. 正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

  13. 除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

    不可查异常

    :::info unchecked exceptions 编译器不要求强制处置的异常 :::

  14. 包括运行时异常(RuntimeException与其子类)和错误(Error)。

    异常基础

    异常关键字

  • try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
  • catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
  • finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
  • throw – 用于抛出异常。
  • throws – 用在方法签名中,用于声明该方法可能抛出的异常。

    异常的声明(throws)

  1. 在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:
    1. public static void method() throws IOException, FileNotFoundException{
    2. //something statements
    3. }
  • 注意:若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常
  1. 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。 ```java private static void readFile(String filePath) throws IOException { File file = new File(filePath); String result; BufferedReader reader = new BufferedReader(new FileReader(file)); while((result = reader.readLine())!=null) {
    1. System.out.println(result);
    } reader.close(); }

3. **Throws抛出异常的规则: **
- 如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
- 必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误
- 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
- 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
4. **含有throws的方法被子类重写时的规则**
```java
/**
 * 子类重写超类含有throws声明异常抛出的方法时对throws的几种特殊的重写规则
 */
public class ThrowsDemo {
    public void dosome()throws IOException, AWTException {}
}
class SubClass extends ThrowsDemo{
//    public void dosome()throws IOException, AWTException {}

    //可以不再抛出任何异常
//    public void dosome(){}

    //可以仅抛出部分异常
//    public void dosome()throws IOException {}

    //可以抛出超类方法抛出异常的子类型异常
//    public void dosome()throws FileNotFoundException {}

    //不允许抛出额外异常(超类方法中没有的,并且没有继承关系的异常)
//    public void dosome()throws SQLException {}

    //不可以抛出超类方法抛出异常的超类型异常
//    public void dosome()throws Exception {}
}

异常的抛出(throw)

  1. 如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示: ```java public static double method(int value) { if(value == 0) {
     throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
    
    } return 5.0 / value; }

1. 大部分情况下都不需要手动抛出异常,因为Java的大部分方法要么已经处理异常,要么已声明异常。所以一般都是捕获异常或者再往上抛。
1. 从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。
```java
private static void readFile(String filePath) throws MyException {    
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    }
}

异常的自定义

定义:

  1. 自定义异常通常用来定义那些业务上的异常问题。

    1. 定义自定义异常需要注意以下问题:
    • 异常的类名要做到见名知义
    • 需要是Exception的子类
    • 提供超类异常提供的所有种类构造器

      非法的年龄异常

      public class IllegalAgeException extends Exception{
      public IllegalAgeException() {
      }
      
      public IllegalAgeException(String message) {
         super(message);
      }
      
      public IllegalAgeException(String message, Throwable cause) {
         super(message, cause);
      }
      
      public IllegalAgeException(Throwable cause) {
         super(cause);
      }
      
      public IllegalAgeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
         super(message, cause, enableSuppression, writableStackTrace);
      }
      }
      
      public class Person {
      private int age;
      
      public int getAge() {
         return age;
      }
      
      /**
      * 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常
      */
      public void setAge(int age) throws IllegalAgeException {
         if(age<0||age>100){
             //使用throw对外抛出一个异常
      //            throw new RuntimeException("年龄不合法!");
             //除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常
      //            throw new Exception("年龄不合法!");
      
             //抛出自定义异常
             throw new IllegalAgeException("年龄超范围:"+age);
      
         }
         this.age = age;
      }
      }
      
      public class ThrowDemo {
      public static void main(String[] args){
         System.out.println("程序开始了...");
         try {
             Person p = new Person();
             /*
                 当我们调用一个含有throws声明异常抛出的方法时,编译器要求
                 我们必须添加处理异常的手段,否则编译不通过.而处理手段有两种
                 1:使用try-catch捕获并处理异常
                 2:在当前方法上继续使用throws声明该异常的抛出
                 具体用哪种取决于异常处理的责任问题
              */
             p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求
             System.out.println("此人年龄:"+p.getAge()+"岁");
         } catch (IllegalAgeException e) {
             e.printStackTrace();
         }
      
         System.out.println("程序结束了...");
      }
      }
      

      异常的捕获

      :::info 异常捕获处理的方法通常有:

  • try-catch
  • try-catch-finally
  • try-finally :::

    try-catch

  1. 在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理 ```java private static void readFile(String filePath) { try {
     // code
    
    } catch (FileNotFoundException e) {
     // handle FileNotFoundException
    
    } catch (IOException e){
     // handle IOException
    
    } }

2. 同一个 catch 也可以捕获多种类型异常,用 | 隔开
```java
private static void readFile(String filePath) {
    try {
        // code
    } catch (FileNotFoundException | UnknownHostException e) {
        // handle FileNotFoundException or UnknownHostException
    } catch (IOException e){
        // handle IOException
    }
}

try-catch-finally

  • 常规语法 ```java try {
    //执行程序代码,可能会出现异常
    } catch(Exception e) {
    //捕获异常并处理
    } finally { //必执行的代码 }

- 执行的顺序 
   - 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
   - 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
   - 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;![1655696525(1).png](https://cdn.nlark.com/yuque/0/2022/png/26235396/1655696532102-2696639a-301d-4eed-ac63-fc8b86527b8c.png#clientId=ud9bc4288-25d0-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=471&id=u48ff3125&margin=%5Bobject%20Object%5D&name=1655696525%281%29.png&originHeight=424&originWidth=564&originalType=binary&ratio=1&rotation=0&showTitle=false&size=208570&status=done&style=none&taskId=u7105b83b-7ace-4164-a3ff-52ee9190ba0&title=&width=626.6666832676644)
<a name="hpyMc"></a>
#### try-finally
:::info
可以直接用try-finally吗? 可以。
:::

1. try块中引起异常,异常代码之后的语句不再执行,直接执行finally语句。 try块没有引发异常,则执行完try块就执行finally语句。
1. try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接操作等等。
```java
//以Lock加锁为例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
    //需要加锁的代码 
} finally {
    lock.unlock(); //保证锁一定被释放
}
  1. finally遇见如下情况不会执行
  • 在前面的代码中用了System.exit()退出程序。
  • finally语句块中发生了异常。
  • 程序所在的线程死亡。
  • 关闭CPU。

    常用的异常

    :::info 在Java中提供了一些异常用来描述经常发生的错误,对于这些异常,有的需要程序员进行捕获处理或声明抛出,有的是由Java虚拟机自动进行捕获处理。Java中常见的异常类: :::

    RuntimeException

  • java.lang.ArrayIndexOutOfBoundsException 数组下标越界异常。当对数组的下标为负数或大于等于数组大小时抛出。

  • java.lang.ArithmeticException 算术条件异常。譬如:整数除零等。
  • java.lang.NullPointerException 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
  • java.lang.ClassNotFoundException 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
  • java.lang.NegativeArraySizeException 数组长度为负异常
  • java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常
  • java.lang.SecurityException 安全性异常
  • java.lang.IllegalArgumentException 非法参数异常

    IOException

  • IOException:操作输入流和输出流时可能出现的异常。

  • EOFException 文件已结束异常
  • FileNotFoundException 文件未找到异常

    其他

  • ClassCastException 类型转换异常类

  • ArrayStoreException 数组中包含不兼容的值抛出的异常
  • SQLException 操作数据库异常类
  • NoSuchFieldException 字段未找到异常
  • NoSuchMethodException 方法未找到抛出的异常
  • NumberFormatException 字符串转换为数字抛出的异常
  • StringIndexOutOfBoundsException 字符串索引超出范围抛出的异常
  • IllegalAccessException 不允许访问某类异常
  • InstantiationException 当应用程序试图使用Class类中的newInstance()方法创建一个类的实例,而指定的类对象无法被实例化时,抛出该异常

    总结:

    异常处理机制是用来处理那些可能存在的异常,但是无法通过修改逻辑完全规避的场景。而如果通过修改逻辑可以规避的异常是bug,不应当用异常处理机制在运行期间解决!应当在编码时及时修正。

    统一处理异常

    :::info 在SpringBoot会有代码的实际引用. :::
  1. Spring MVC框架提供了统一处理异常的机制,使得特定种类的异常对应 一段特定的代码,后续,当编写代码时,无论在任何位置,都可以将异常 直接抛出,由统一处理异常的代码进行处理即可
  • – 无论哪种异常(包括RuntimeException及其子孙类异常),只要没有显式的使用 try…catch语法进行捕获并处理,均视为抛出
  1. 关于统一处理异常,需要自定义方法对异常进行处理,关于此方法:
  • – 注解:必须添加@ExceptionHandler注解
  • – 访问权限:应该是公有的 – 返回值类型:可参考处理请求的方法的返回值类型
  • – 方法名称:自定义
  • – 参数列表:必须包含1个异常类型的参数,并且可按需添加HttpServletRequest、 HttpServletResponse等少量特定的类型的参数,不可以随意添加参数
  1. 为保证更合理的处理异常,应该:
  • – 将处理异常的代码放在专门的类中
  • – 在此类上添加@ControllerAdvice注解
  • 由于目前主流的响应方式都是“响应正文”的,则可以将@ControllerAdvice替换为 @RestControllerAdvice
  1. Spring MVC允许存在多个统一处理异常的方法,这些方法可以在不同的 类中,只要处理的异常的类型不冲突即可(允许继承)
  • – 例如:如果有2个或多个方法都处理NullPointerException,是错误的
  • – 例如:如果同时存在2个方法,分别处理NullPointerException和 RuntimeException,是允许的
  1. @ExceptionHandler注解还可用于配置被注解的方法能够处理的异常的类型,其效力的优先级高于在方法的参数上指定异常类型
  2. 在开发实践中,建议为每一个@ExceptionHandler配置注解参数,在注解参数中指定需要处理异常的类型,而处理异常的方法的参数类型只需要包含@ExceptionHandler配置的类型即可,甚至可以是Throwable
  3. 在开发实践中,通常都会有handleThrowable()方法(方法名是自定义 的),以避免某个异常没有被处理而导致500错误!
  • – 此方法中应该输出异常的相关信息,甚至跟踪信息,否则,当程序运行此至处时,可能不便于观察、分析、记录出现异常 ```java package cn.chenjava.springmvc.controller.handler;

import cn.chenjava.springmvc.vo.UserVO; import org.springframework.remoting.RemoteTimeoutException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.io.IOException;

@RestControllerAdvice//下面两个的合体,注释后就会只作用于控制类 // @ControllerAdvice,如果不是只作用于响应正文的话,用这个,但是几乎很少 // @ResponseBody public class GlobalExceptionHandler { //注释,参数可以传进去多个打算共同处理的无关异常 @ExceptionHandler({NullPointerException.class, NumberFormatException.class}) //注意,这里的参数类型要包含 public String handleException(Throwable e) { return “Error, “ + e.getClass().getName() + “, message : “ + e.getMessage(); } //这是分开处理,上面是一起处理,两个不能共存!!!!!!!!!!!!!! @ExceptionHandler public String handleNullPointerException(NullPointerException e) { return “Error, NullPointerException!”; } @ExceptionHandler public String handleNumberFormatException(NumberFormatException e) { return “Error, NumberFormatException!”; }

//这个是兜底的,以避免某个异常没有被处理而导致500错误!
//此方法中应该输出异常的相关信息,甚至跟踪信息,
//否则,当程序运行此至处时,可能不便于观察、分析、记录出现异常
@ExceptionHandler
public String handleThrowable(Throwable e) {
    e.printStackTrace();
    return "Error, " + e.getClass().getName() + ", message : " + e.getMessage();
}

//以上代码其实;类似于下面这样的, // try { // ???.???(xx); // } catch (NullPointerException e) { // handleException(e); // } catch (FileNotFoundException e) { // } catch (IOException e) { // }

} ```