JavaSE6️⃣异常(Throwable)
异常:程序在运行期间发生的特殊事件(问题或错误),干扰程序的正常执行。
可以是编程错误、非法参数、输入输出错误、网络问题、资源耗尽等。
常见的异常处理方式:
-
约定错误码:对于错误情况,约定
int
类型的错误码(处理起来较麻烦,常见于底层 C 函数)。 -
提供异常处理机制:Java 提供了异常处理机制,使程序可以抛出、捕获和处理异常,避免程序的崩溃或异常退出。
1、异常体系
java.lang.Throwable
(异常体系的超类)包含线程执行堆栈的快照,
提供
printStackTrace()
等 API 用于获取堆栈信息。
1.1、API 体系
在 API 层面,异常分为两大体系。
- Error:严重错误(如 JVM、硬件资源等),无法恢复。
- StackOverFlowError、OutOfMemoryError
- NoClassDefFoundError
- ...
- Exception:程序编译或运行时产生的异常,程序本身可处理。
- 运行时异常:RuntimeException 及其子类。
- NullPointerException
- IndexOutOfBoundsException
- ClassCastException
- ...
- 编译时异常:非 RuntimeException 异常。
- IOException
- SQLException
- ...
- 运行时异常:RuntimeException 及其子类。
1.2、可查类型
根据 Java 编译器是否可查,异常分为两类。
非受检异常(unchecked) | 受检异常(checked) | |
---|---|---|
可查 | ❌ | ✔ |
异常处理 | 不强制要求处理 | 要求必须处理 |
类型 | Error 、RuntimeException 及子类 |
编译时异常 |
不处理的后果 | 程序崩溃或异常退出 | 编译器报错 |
- 编译器仅能检查出编译时异常,用于检查程序本身逻辑是否有误。
- 虽然 RuntimeException 属于非受检异常,应结合具体场景决定是否处理,避免程序崩溃。
2、异常使用
关键字
-
语句块:
含义 作用 try 尝试(监听) 尝试执行可能发生异常的代码 catch 捕获 捕获 try 块中相应类型的异常,根据需要进行处理 finally 最终执行 执行一些必要代码(无论是否发生异常) -
抛出:
含义 作用 throw 抛出 手动抛出异常 throws 声明方法抛出异常 在方法签名中,声明此方法中可能抛出的未捕获异常类型
2.1、异常产生
异常产生:可能由系统产生,也可以是人为产生。
- 系统产生:程序运行时遇到不合规范(预期之外)的代码或结果时,自动产生异常。
- 手动抛出:使用
throw
关键字,手动抛出异常。- 若在一个异常语句块中多次抛出异常,则后抛出的会覆盖先抛出的。
- 假如抛出异常没有被捕获,则必须在方法体声明
throws
抛给方法调用者。
2.2、异常处理
异常处理:受检异常(checked)必须处理,非受检异常可不处理。
处理方式:
- 捕获:使用
catch
捕获异常。 - 向上抛出:在方法签名使用
throws
关键字,将异常抛给方法调用者。
2.3、异常传播
当发生异常时,每一级调用栈都可以选择捕获或继续向上抛出。
向上抛出后,异常沿着方法调用链反向传递。
- 传播中止:直到异常被捕获,或传递到最外层调用者(
main
方法)。 - 默认异常处理:最外层调用者(
main
方法)没有捕获异常,按以下顺序处理:- 打印异常跟踪信息:调用异常对象的
printStackTrace()
。 - 终止程序:调用异常处理器终止程序。
- 默认异常处理器:
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
)
-
含义:在处理异常时,新产生的异常会覆盖原有的异常。
主异常 受抑制异常 含义 导致其它异常发生的异常 异常处理过程中产生的新异常 说明 异常处理的核心,未解决会导致程序崩溃 通常是资源关闭等操作引起,应当被抑制以保证程序的正常执行 处理方式 优先尽快解决 不立即抛出,而是添加到主异常的受抑制异常列表中,以便稍后处理 -
原因及后果:
- 在
catch
或finally
块中,新抛出的异常没有相应的catch
捕获,会直接向上抛出。 - 方法调用者只能看到后抛出的异常,难以调试和排查问题。
- 在
Throwable
成员(Java 1.7+)
-
suppressedExceptions:受抑制异常列表。
-
addSuppressed():向当前异常添加一个受抑制异常。
-
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
块可捕获单个异常类型,可执行任何合法代码来处理异常。
-
执行流程:当
try
块发生异常时,- 按
catch
块的声明顺序依次匹配。 - 根据是否匹配到相应异常类型(或父类/接口类型):
- 是:跳转到对应
catch
块执行。 - 否:向上抛出异常。
- 是:跳转到对应
- 按
-
多个 catch 块:
-
可使用多个
catch
块声明可能发生的异常类型。 -
按顺序声明,优先声明最具体的异常类型(先子后父,先小后大)。
try { // ... } catch (FileNotFoundException e) { // ... } catch (IOException e) { // ... }
-
multi-catch
允许
catch
捕获多个异常类型(Java 1.7+)
-
使用场景:多个异常类型的处理逻辑完全相同(否则需要分开定义多个
catch
块)。 -
要求:
-
使用
|
分隔多个异常类型。 -
多个异常类型之间没有顺序要求,但不能存在继承关系(否则编译失败)。
try { // ... } catch (FileNotFoundException | NullPointerException e) { // ... }
-
3.2、t-f
-
使用场景:不捕获异常,通常执行一些清理操作(如释放连接)。
-
执行流程:根据
try
块是否发生异常-
是:执行
try
块 → 执行finally
块 → 抛出异常由上层处理。 -
否:执行
try
块直到发生异常 → 执行finally
块 → 结束语句块。try { // ... } finally { // ... }
-
3.3、t-c-f(❗)
t-c
和t-f
的结合版(常用)捕获可能抛出的异常并处理,且保证在任何情况下都执行特定代码。
执行流程:根据 try
块是否发生异常
- 是:执行 try 块直到发生异常 → 按 catch 块的声明顺序依次匹配。
- 匹配:跳转到对应
catch
块执行 → 执行finally
→ 结束语句块。 - 否:执行
finally
块 → 向上抛出异常。
- 匹配:跳转到对应
- 否:执行
try
块 → 执行finally
→ 结束语句块。
3.4、t-w-r(❗)
Java 1.7+
用于管理资源的自动释放。
-
语法:在
try
块的()
中声明资源对象,使用;
分隔。try (资源1; 资源2; ...) { // ... } catch (Exception e) { // ... }
-
说明:
- 资源类必须实现
AutoCloseable
或Closeable
接口。 - 编译器自动在
try
内部生成finally
块,按顺序自动调用资源的close()
,无需手动关闭(资源先开后关)。 - 在
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()
发生异常:异常会受抑制,直到所有资源释放完成后,抛出一个主异常(包含所有抑制异常)。
-
4、自定义异常
在程序中需要抛出异常时,尽量使用 JDK 内置异常类型。
在大型项目中,有必要制定一个合理的自定义异常体系。
4.1、步骤
必要步骤
- 创建一个类:继承现有的继承类,通常是
Exception
或RuntimeException
。 - 定义构造方法:
super()
调用父类构造方法,传入参数即可。- 无参
- msg:异常信息。
- cause:原因异常。
- ...
可选步骤
- 重写方法:如 getMessage()/toString()
- 添加自定义属性或方法。
4.2、最佳实践
-
根异常:定义一个
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); } // ... }
-
业务异常:基于根异常,派生各种业务类型的异常。
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
不会正常执行。
-
程序所在线程死亡。
-
JVM 终止:
- 退出:
System.exit()
- 崩溃:StackOverFlowError、OOM 等
- 退出:
-
执行 finally 块时发生异常:异常之后的剩余代码不会被执行。
return
异常掩盖
-
假设在
try
块发生异常,但finally
块使用了return
语句。 -
调用者只能观察到
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
类中,涉及以下结构。
-
函数式接口:方法
uncaughtException()
用于处理线程中未捕获的异常。@FunctionalInterface public interface UncaughtExceptionHandler { void uncaughtException(Thread t, Throwable e); }
-
成员变量:
Thread
类中定义的成员变量,用于设置线程的未捕获异常处理器。private volatile UncaughtExceptionHandler uncaughtExceptionHandler; private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
处理流程
当线程中出现未捕获的异常时,JVM 首先检查 uncaughtExceptionHandler
是否存在。
-
是:调用其
uncaughtException()
处理。 -
否:调用
defaultUncaughtExceptionHandler.uncaughtException()
的方法处理。uncaughtExceptionHandler defaultUncaughtExceptionHandler static ❌ ✔ 作用 处理当前线程对象中的未捕获异常 处理所有线程对象中的未捕获异常 优先级 更高 - 默认值 null
JVM 底层提供,方法实现为 System.exit(状态码)
自定义异常处理器
-
创建一个类:实现
UncaughtExceptionHandler
接口及方法。public class MyExceptionHandler implements UncaughtExceptionHandler{ @override void uncaughtException(Thread t, Throwable e) { // 处理逻辑 } }
-
设置异常处理器:调用当前线程的相关
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)