JavaSE6️⃣异常(Throwable)

异常:程序在运行期间发生的特殊事件(问题或错误),干扰程序的正常执行。

可以是编程错误、非法参数、输入输出错误、网络问题、资源耗尽等。

常见的异常处理方式:

  1. 约定错误码:对于错误情况,约定 int 类型的错误码(处理起来较麻烦,常见于底层 C 函数)。

  2. 提供异常处理机制:Java 提供了异常处理机制,使程序可以抛出、捕获和处理异常,避免程序的崩溃或异常退出

    image-20220419122429335

1、异常体系

java.lang.Throwable(异常体系的超类)

包含线程执行堆栈的快照,

提供 printStackTrace() 等 API 用于获取堆栈信息。

1.1、API 体系

在 API 层面,异常分为两大体系。

  • Error:严重错误(如 JVM、硬件资源等),无法恢复
    • StackOverFlowError、OutOfMemoryError
    • NoClassDefFoundError
    • ...
  • Exception:程序编译或运行时产生的异常,程序本身可处理
    • 运行时异常RuntimeException 及其子类。
      • NullPointerException
      • IndexOutOfBoundsException
      • ClassCastException
      • ...
    • 编译时异常:非 RuntimeException 异常。
      • IOException
      • SQLException
      • ...

1.2、可查类型

根据 Java 编译器是否可查,异常分为两类。

非受检异常(unchecked) 受检异常(checked)
可查
异常处理 不强制要求处理 要求必须处理
类型 ErrorRuntimeException 及子类 编译时异常
不处理的后果 程序崩溃或异常退出 编译器报错
  • 编译器仅能检查出编译时异常,用于检查程序本身逻辑是否有误。
  • 虽然 RuntimeException 属于非受检异常,应结合具体场景决定是否处理,避免程序崩溃。

2、异常使用

关键字

  1. 语句块

    含义 作用
    try 尝试(监听) 尝试执行可能发生异常的代码
    catch 捕获 捕获 try 块中相应类型的异常,根据需要进行处理
    finally 最终执行 执行一些必要代码(无论是否发生异常)
  2. 抛出

    含义 作用
    throw 抛出 手动抛出异常
    throws 声明方法抛出异常 在方法签名中,声明此方法中可能抛出的未捕获异常类型

2.1、异常产生

异常产生:可能由系统产生,也可以是人为产生。

  • 系统产生:程序运行时遇到不合规范(预期之外)的代码或结果时,自动产生异常。
  • 手动抛出:使用 throw 关键字,手动抛出异常。
    1. 若在一个异常语句块中多次抛出异常,则后抛出的会覆盖先抛出的
    2. 假如抛出异常没有被捕获,则必须在方法体声明 throws 抛给方法调用者。

2.2、异常处理

异常处理受检异常(checked)必须处理,非受检异常可不处理。

处理方式

  1. 捕获:使用 catch 捕获异常。
  2. 向上抛出:在方法签名使用 throws 关键字,将异常抛给方法调用者。

2.3、异常传播

当发生异常时,每一级调用栈都可以选择捕获或继续向上抛出

向上抛出后,异常沿着方法调用链反向传递

  1. 传播中止:直到异常被捕获,或传递到最外层调用者(main 方法)
  2. 默认异常处理最外层调用者(main 方法)没有捕获异常,按以下顺序处理:
    1. 打印异常跟踪信息:调用异常对象的 printStackTrace()
    2. 终止程序:调用异常处理器终止程序。
      • 默认异常处理器System.exit(状态码)
      • 自定义异常处理器

异常链 - cause

Throwable 成员(Java 1.4+)

将异常与原始异常相关联,形成异常链。

  • cause:表示导致当前异常的原因,称为原因异常

  • getCause():可获取异常的完整上下文信息。

    null 表示当前为根源异常,没有关联的原因异常)

    // 成员变量
    private Throwable cause = this;
    
    // 获取
    public synchronized Throwable getCause() {
        return (cause==this ? null : cause);
    }
    

异常掩盖 - suppressed

异常掩盖(Exception Suppression

  1. 含义:在处理异常时,新产生的异常会覆盖原有的异常。

    主异常 受抑制异常
    含义 导致其它异常发生的异常 异常处理过程中产生的新异常
    说明 异常处理的核心,未解决会导致程序崩溃 通常是资源关闭等操作引起,应当被抑制以保证程序的正常执行
    处理方式 优先尽快解决 不立即抛出,而是添加到主异常的受抑制异常列表中,以便稍后处理
  2. 原因及后果

    1. catchfinally 块中,新抛出的异常没有相应的 catch 捕获,会直接向上抛出。
    2. 方法调用者只能看到后抛出的异常,难以调试和排查问题。

Throwable 成员(Java 1.7+)

  1. suppressedExceptions:受抑制异常列表。

  2. addSuppressed():向当前异常添加一个受抑制异常。

  3. getSuppressed():以数组形式返回所有的受抑制异常。

    public class Throwable implements Serializable {
        private List<Throwable> suppressedExceptions = SUPPRESSED_SENTINEL;
    
        public final synchronized void addSuppressed(Throwable exception) {}
    
        public final synchronized Throwable[] getSuppressed() {}
    }
    

3、语句块

3.1、t-c

catch

catch 块可捕获单个异常类型,

可执行任何合法代码来处理异常。

  1. 执行流程:当 try 块发生异常时,

    1. catch 块的声明顺序依次匹配
    2. 根据是否匹配到相应异常类型(或父类/接口类型):
      1. :跳转到对应 catch 块执行。
      2. :向上抛出异常。
  2. 多个 catch 块

    1. 可使用多个 catch 块声明可能发生的异常类型。

    2. 按顺序声明,优先声明最具体的异常类型(先子后父,先小后大)。

      try {
          // ...
      } catch (FileNotFoundException e) {
          // ...
      } catch (IOException e) {
          // ...
      }
      

multi-catch

允许 catch 捕获多个异常类型(Java 1.7+)

  1. 使用场景:多个异常类型的处理逻辑完全相同(否则需要分开定义多个 catch 块)。

  2. 要求

    1. 使用 | 分隔多个异常类型。

    2. 多个异常类型之间没有顺序要求,但不能存在继承关系(否则编译失败)。

      try {
          // ...
      } catch (FileNotFoundException | NullPointerException e) {
          // ...
      }
      

3.2、t-f

  1. 使用场景:不捕获异常,通常执行一些清理操作(如释放连接)。

  2. 执行流程:根据 try 块是否发生异常

    1. :执行 try 块 → 执行 finally 块 → 抛出异常由上层处理。

    2. :执行 try 块直到发生异常 → 执行 finally 块 → 结束语句块。

      try {
          // ...
      } finally {
          // ...
      }
      

3.3、t-c-f(❗)

t-ct-f 的结合版(常用)

捕获可能抛出的异常并处理,且保证在任何情况下都执行特定代码。

执行流程:根据 try 块是否发生异常

  1. :执行 try 块直到发生异常 → 按 catch 块的声明顺序依次匹配。
    1. 匹配:跳转到对应 catch 块执行 → 执行 finally → 结束语句块。
    2. :执行 finally 块 → 向上抛出异常。
  2. :执行 try 块 → 执行 finally → 结束语句块。

3.4、t-w-r(❗)

Java 1.7+

用于管理资源的自动释放。

  1. 语法:在 try 块的 () 中声明资源对象,使用 ; 分隔。

    try (资源1; 资源2; ...) {
        // ...
    } catch (Exception e) {
        // ...
    }
    
  2. 说明

    1. 资源类必须实现 AutoCloseableCloseable 接口。
    2. 编译器自动try 内部生成 finally,按顺序自动调用资源的 close(),无需手动关闭(资源先开后关)。
    3. t-w-r 语句块中,无论在哪个阶段发生异常,资源都会被正常释放

示例

  • 代码

    try(InputStream is = new FileInputStream("d:\\1.txt")) {
        // 可能发生异常的代码
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  • 等价于try 块内部生成一个 t-c-f 语句块,用于执行可能发生异常的代码。

    try {
        InputStream is = new FileInputStream("d:\\1.txt");
    
        Throwable t = null;
    
        try {
            // 可能发生异常的代码
        } catch (Throwable e1) {
            t = e1;
            throw e1;
        } finally {
            if (is != null) {
                if (t != null) {	// try块发生异常
                    try {
                        is.close();
                    } catch (Throwable e2) {
                        t.addSuppressed(e2);// 受抑制异常
                    }
                } else {
                    is.close();	// 受抑制,直到资源释放完成
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  • 执行流程图示配合代码食用

    • tryclose() 都发生异常:前者是主异常,后者是受抑制异常。

    • 若仅 try 块发生异常:资源释放后被捕获。

    • 若仅 close() 发生异常:异常会受抑制,直到所有资源释放完成后,抛出一个主异常(包含所有抑制异常)。

      image

4、自定义异常

在程序中需要抛出异常时,尽量使用 JDK 内置异常类型

在大型项目中,有必要制定一个合理的自定义异常体系。

4.1、步骤

必要步骤

  1. 创建一个类:继承现有的继承类,通常是 ExceptionRuntimeException
  2. 定义构造方法super() 调用父类构造方法,传入参数即可。
    • 无参
    • msg:异常信息。
    • cause:原因异常。
    • ...

可选步骤

  1. 重写方法:如 getMessage()/toString()
  2. 添加自定义属性或方法。

4.2、最佳实践

  1. 根异常:定义一个 BaseException 作为根异常,提供多个构造方法和必要的属性或方法。

    public class BaseException extends RuntimeException {
        public BaseException() {
            super();
        }
        public BaseException(String message) {
            super(message);
        }
        public BaseException(Throwable cause) {
            super(cause);
        }
        public BaseException(String message, Throwable cause) {
            super(message, cause);
        }
        // ...
    }
    
  2. 业务异常:基于根异常,派生各种业务类型的异常。

    public class UserException extends BaseException {}
    public class UserInfoException extends UserException {}
    
    public class TransactionException extends BaseException {}
    public class TransactionInfoException extends TransactionException {}
    

5、说明

5.1、finally 特殊情况

非正常执行

在执行 finally 之前,通常是在 try 块中。

发生以下情况时,finally 不会正常执行。

  1. 程序所在线程死亡

  2. JVM 终止

    1. 退出System.exit()
    2. 崩溃:StackOverFlowError、OOM 等
  3. 执行 finally 块时发生异常:异常之后的剩余代码不会被执行。

return

异常掩盖

  1. 假设在 try 块发生异常,但 finally 块使用了 return 语句。

  2. 调用者只能观察到 finally 的返回值,无法感知是否发生了异常。

    public boolean foo() {
        try {
            // 假设该代码抛出异常
        } finally {
            // ...
            return true;
        }
    }
    

锁定变量返回值

假设 try 块返回某个变量 a,根据 finally 块中是否使用了 return 语句。

  • 使用 return:返回 finally 中的值,无论是否为变量 a。

    // 返回 20
    public int foo() {
        int a;
    
        try {
            a = 10;
            return a;
        } finally {
            a = 20;	// 修改变量
            return a;
        }
    }
    
  • 未使用 returnfinally 块中对 A 的修改不会影响返回值原理 👉 字节码技术 5.5

    // 返回 10
    public int foo() {
        int a;
    
        try {
            a = 10;
            return a;
        } finally {
            a = 20;
            // return a;
        }
    }
    

5.2、异常处理器

API

UncaughtExceptionHandler(未捕获异常处理器)

定义于 Thread 类中,涉及以下结构。

  1. 函数式接口:方法 uncaughtException() 用于处理线程中未捕获的异常。

    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        void uncaughtException(Thread t, Throwable e);
    }
    
  2. 成员变量Thread 类中定义的成员变量,用于设置线程的未捕获异常处理器。

    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
    
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
    

处理流程

当线程中出现未捕获的异常时,JVM 首先检查 uncaughtExceptionHandler 是否存在。

  • :调用其 uncaughtException() 处理。

  • :调用 defaultUncaughtExceptionHandler.uncaughtException() 的方法处理。

    uncaughtExceptionHandler defaultUncaughtExceptionHandler
    static
    作用 处理当前线程对象中的未捕获异常 处理所有线程对象中的未捕获异常
    优先级 更高 -
    默认值 null JVM 底层提供,方法实现为 System.exit(状态码)

自定义异常处理器

  1. 创建一个类:实现 UncaughtExceptionHandler 接口及方法。

    public class MyExceptionHandler
        implements UncaughtExceptionHandler{
        @override
        void uncaughtException(Thread t, Throwable e) {
            // 处理逻辑
        }
    }
    
  2. 设置异常处理器:调用当前线程的相关 setXxx() 方法,设置自定义异常处理器的实例。

    // 当前线程对象
    Thread.currentThread().setUncaughtExceptionHandler(new MyExceptionHanddler);
    
    // 所有线程对象
    Thread.setDefaultExceptionHandler(new MyExceptionHanddler);
    

5.3、printStackTrace()

打印异常跟踪信息(堆栈轨迹),帮助开发者调试程序。

格式源文件.方法名(类名:行号)

一般形式

从下往上,是方法的调用栈顺序。

最上面的调用栈,可以定位到异常产生的位置。

示例:各个方法按以下顺序依次调用。

  • Demo.java 的 main()

  • Demo.java 的 process()

  • Integer 的 parseInt(String)

  • Integer 的 parseInt(String, int)

    java.lang.NumberFormatException: null
        at java.lang.Integer.parseInt(Integer.java:542)
        at java.lang.Integer.parseInt(Integer.java:615)
        at Demo.process(Demo.java:19)
        at Demo.main(Demo.java:3)
    

cause

若异常对象的 cause 不为 null

调用 printStackTrace() 的效果如下。

java.lang.IllegalArgumentException: java.lang.NullPointerException
    at Main.process1(Main.java:15)
    at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
    at Main.process2(Main.java:20)
    at Main.process1(Main.java:13)

suppressed

若异常对象包含了受抑制异常,

调用 printStackTrace() 的效果如下。

Exception in thread "main" java.lang.IllegalArgumentException
    at Main.main(Main.java:11)
Suppressed: java.lang.NumberFormatException: For input string: "abc"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:652)
    at java.lang.Integer.parseInt(Integer.java:770)
    at Main.main(Main.java:6)
posted @ 2023-03-06 01:49  Jaywee  阅读(154)  评论(0编辑  收藏  举报

👇