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)
可查 ❌ ✔
异常处理 不强制要求处理 要求必须处理
类型 Error、RuntimeException 及子类 编译时异常
不处理的后果 程序崩溃或异常退出 编译器报错
  • 编译器仅能检查出编译时异常,用于检查程序本身逻辑是否有误。
  • 虽然 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. 在 catch 或 finally 块中,新抛出的异常没有相应的 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-c 和 t-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. 资源类必须实现 AutoCloseable 或 Closeable 接口。
    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();
    }
    
  • 执行流程图示:配合代码食用

    • 若 try 和 close() 都发生异常:前者是主异常,后者是受抑制异常。

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

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

      image

4、自定义异常

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

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

4.1、步骤

必要步骤

  1. 创建一个类:继承现有的继承类,通常是 Exception 或 RuntimeException。
  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;
        }
    }
    
  • 未使用 return:finally 块中对 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  阅读(250)  评论(0)    收藏  举报

☝

👇