Exception
字数: 0 字 时长: 0 分钟
1. 异常处理全面解析
1.1 什么是异常
异常(Exception)可以理解为:
程序运行过程中出现的非正常情况,它会改变程序原本的正常执行流程。
常见的异常场景包括:
文件不存在
网络连接失败
数据库访问失败
数组越界
空指针
类型转换错误
参数非法
除数为 01.1.1 异常对程序执行流程的影响
例如:
public class Demo {
public static void main(String[] args) {
System.out.println(
"开始执行"
);
System.out.println(
10 / 0
);
System.out.println(
"执行结束"
);
}
}执行:
10 / 0会抛出:
Exception in thread "main"
java.lang.ArithmeticException: / by zero执行过程:
开始执行
↓
10 / 0
↓
ArithmeticException
↓
正常执行流程被打断因此后面的:
System.out.println(
"执行结束"
);不会继续执行。
如果异常一直没有被处理:
异常发生
↓
当前正常执行流程被打断
↓
异常沿调用栈向上传播
↓
如果最终仍然没人处理
↓
当前线程终止如果使用异常处理:
public class Demo {
public static void main(String[] args) {
try {
System.out.println(
10 / 0
);
} catch (
ArithmeticException e) {
System.out.println(
"除数不能为 0"
);
}
System.out.println(
"程序继续执行"
);
}
}输出:
除数不能为 0
程序继续执行所以异常处理并不是:
异常发生以后什么都没发生而是:
异常发生以后,由程序决定应该如何处理这次异常。
Java 的异常处理机制可以帮助我们:
捕获异常
记录异常
进行补偿
重试操作
释放资源
继续向上抛出
转换成业务异常1.1.2 Java 异常体系
Java 异常体系的根类是:
java.lang.Throwable整体结构可以简单理解:
Throwable
├── Error
│
└── Exception
├── RuntimeException
│ ├── NullPointerException
│ ├── ArithmeticException
│ ├── ClassCastException
│ ├── IndexOutOfBoundsException
│ └── ...
│
└── 其他 Exception
├── IOException
├── SQLException
├── ClassNotFoundException
└── ...只有:
Throwable以及它的子类对象才能:
被 throw 抛出
被 catch 捕获例如:
throw new RuntimeException(
"发生异常"
);1.1.3 异常信息与异常堆栈
异常发生以后,通常会看到类似:
java.lang.ArithmeticException: / by zero
at Demo.test(Demo.java:20)
at Demo.main(Demo.java:10)这里包含:
异常类型
异常信息
异常发生位置
方法调用链这些信息统称为:
异常堆栈
Stack Trace对于普通用户来说,通常不应该直接看到:
NullPointerException
SQLException
IOException这样的底层异常。
而对于开发人员来说:
异常类型、异常消息和异常堆栈都是定位问题的重要信息,不应该随意丢失。
开发阶段可以:
e.printStackTrace();打印堆栈。
实际项目中通常更推荐使用日志框架:
log.error(
"业务处理失败",
e
);这样能够保留:
异常类型
异常消息
完整调用栈
时间
线程
服务实例等信息。
1.2 Exception 和 Error 的区别
Throwable 主要分为:
Error
Exception虽然两者都属于异常体系,但含义和处理方式并不一样。
1.2.1 Error
Error 通常表示:
JVM 或运行环境发生了比较严重的问题。
常见的:
OutOfMemoryError
StackOverflowError
NoClassDefFoundError例如:
OutOfMemoryError
↓
JVM 无法获得足够内存这种问题通常不是普通业务代码通过:
try-catch就能够真正解决的。
所以:
Error 一般不作为正常业务异常处理机制的一部分。
1.2.2 Exception
Exception 表示程序运行过程中可能出现的异常情况。
例如:
IOException
SQLException
ClassNotFoundException
NullPointerException
ArithmeticException其中:
IOException
SQLException
ClassNotFoundException通常属于:
Checked Exception而:
NullPointerException
ArithmeticException
ClassCastException属于:
RuntimeException
↓
Unchecked Exception1.2.3 Exception 和 Error 对比
可以简单理解:
Throwable
├── Error
│ ↓
│ JVM / 系统层面的严重问题
│
└── Exception
↓
程序运行过程中需要考虑的异常情况不过不能简单认为:
Error
↓
百分之百无法处理
Exception
↓
百分之百可以恢复是否能够恢复,最终还是取决于具体场景。
1.3 Checked 和 Unchecked 异常
Java 异常中一个非常重要的分类就是:
Checked Exception
Unchecked Exception
1.3.1 Checked Exception
Checked Exception 通常翻译为:
检查型异常
受检异常最大的特点是:
编译器要求必须处理。
处理方式只有两种:
try-catch 捕获
或者
throws 声明抛出否则无法通过编译。
常见的 Checked Exception:
IOException
SQLException
ClassNotFoundException例如:
Class.forName(
"com.kerwim.Demo.Demo1"
);Class.forName() 可能抛出:
ClassNotFoundException如果完全不处理,IDE 会提示:

可以使用:
try {
Class<?> clazz =
Class.forName(
"com.kerwim.Demo.Demo1"
);
} catch (
ClassNotFoundException e) {
e.printStackTrace();
}也可以声明:
public static void main(
String[] args)
throws ClassNotFoundException {
Class<?> clazz =
Class.forName(
"com.kerwim.Demo.Demo1"
);
}所以可以简单理解:
Checked Exception
↓
编译器强制你考虑
↓
catch
或者
throws1.3.2 Unchecked Exception
Unchecked Exception 指:
编译器不强制要求显式捕获或者声明的异常。
RuntimeException 以及它的子类都是典型的 Unchecked Exception。
例如:
NullPointerException
ArithmeticException
ClassCastException
ArrayIndexOutOfBoundsException
IllegalArgumentException例如:
int result =
10 / 0;编译器允许通过。
但是运行时会抛出:
ArithmeticException再例如:
String name = null;
System.out.println(
name.length()
);编译器同样不会要求:
try-catch但运行时可能抛出:
NullPointerException所以:
Unchecked Exception
↓
编译器不强制处理
↓
很多时候应该通过正确编码提前避免1.3.3 Checked 和 Unchecked 的区别
| 对比 | Checked Exception | Unchecked Exception |
|---|---|---|
| 编译器是否强制处理 | 是 | 否 |
是否必须 catch 或 throws | 是 | 否 |
| 常见类型 | IOException、SQLException | NullPointerException、IllegalArgumentException |
| 常见来源 | IO、数据库、外部资源等 | 参数错误、逻辑错误、代码缺陷等 |
| 类型关系 | Exception 中除 RuntimeException 体系外 | RuntimeException 及其子类 |
不能简单理解成:
Checked
↓
一定可以恢复
Unchecked
↓
一定是程序员写错了异常是否可以恢复,需要根据具体业务判断。
另外,从整个:
Throwable体系来看:
Error同样不要求编译器强制捕获。
不过日常讨论:
Checked / Unchecked Exception时,重点通常是 Exception 体系内部的分类。
1.3.4 NoClassDefFoundError 和 ClassNotFoundException
这两个名字非常相似:
NoClassDefFoundError
ClassNotFoundException但属于完全不同的体系。
ClassNotFoundException:
Exception
↓
Checked Exception典型场景:
Class.forName(
"com.kerwim.User"
);程序主动动态加载某个类,但是没有找到。
常见原因:
类名写错
依赖不存在
目标 Class 不在 ClassPathNoClassDefFoundError:
Error通常意味着:
JVM 在运行过程中需要某个本应该存在的类,但此时无法正常获得它。
例如:
编译时依赖存在
运行时 jar 包缺失另外,某个类初始化失败以后,后续再次使用这个类时,也可能出现:
NoClassDefFoundError可以简单区分:
ClassNotFoundException
↓
主动加载类的时候没找到
NoClassDefFoundError
↓
JVM 运行过程中需要某个类
↓
却无法正常获得1.4 throw 和 throws
Java 中有两个非常容易混淆的关键字:
throw
throws虽然只差一个:
s但作用完全不同。
throw
↓
真正抛出异常对象
throws
↓
声明方法可能向外抛出异常1.4.1 throw
throw 用于:
主动抛出一个异常对象。
基本语法:
throw new 异常类型(
"异常信息"
);例如:
throw new IllegalArgumentException(
"参数不能为空"
);再例如:
public class ThrowDemo {
public static void checkEligibility(
int age) {
if (age < 18) {
throw new IllegalArgumentException(
"年龄未满 18 岁,禁止观影"
);
}
System.out.println(
"请认真观影"
);
}
public static void main(
String[] args) {
checkEligibility(10);
System.out.println(
"愉快的周末"
);
}
}当:
age < 18时,会主动执行:
throw new IllegalArgumentException(...)如果没有被捕获,后面的:
System.out.println(
"愉快的周末"
);不会继续正常执行。

1.4.2 throws
throws 用于:
方法声明例如:
public void method()
throws IOException {
}表示:
这个方法执行过程中可能把指定异常继续抛给调用者处理。
例如:
public static void loadClass()
throws ClassNotFoundException {
Class.forName(
"com.kerwim.Demo.Demo1"
);
}此时:
loadClass()
↓
自己不处理
↓
把异常继续交给调用者调用方可以:
try {
loadClass();
} catch (
ClassNotFoundException e) {
e.printStackTrace();
}对于 Checked Exception,如果既没有:
catch也没有:
throws编译器会直接报错。

1.4.3 throws 声明多个异常
throws 可以同时声明多个异常。
例如:
public void execute()
throws IOException,
SQLException {
}表示:
execute()
↓
可能向调用者传播 IOException
↓
也可能传播 SQLException多个异常之间使用:
,分隔。
1.4.4 throw 和 throws 的区别
| 对比 | throw | throws |
|---|---|---|
| 作用 | 真正抛出异常 | 声明可能抛出的异常 |
| 使用位置 | 方法体内部 | 方法签名 |
| 后面跟什么 | 异常对象 | 异常类型 |
| 数量 | 一次抛出一个对象 | 可以声明多个异常 |
| 示例 | throw new RuntimeException() | throws IOException |
例如:
throw new ArithmeticException(
"算术异常"
);而:
public void test()
throws ArithmeticException {
}可以简单记成:
throw
↓
扔异常
throws
↓
声明我可能往外扔1.4.5 什么时候 catch,什么时候 throws
没有绝对规则。
最重要的判断是:
当前这一层有没有能力正确处理这个异常。
如果当前方法能够真正进行:
重试
降级
补偿
返回默认值
转换业务结果可以考虑:
catch如果当前层不知道应该如何处理:
不知道怎么恢复
↓
继续向上传播通常更合理。
不要为了消除编译报错而写:
try {
// ...
} catch (
Exception e) {
}然后什么都不做。
这叫:
吞异常会导致:
上层不知道失败
日志没有记录
真正的问题被隐藏
排查非常困难1.5 try-catch-finally
Java 最核心的异常处理结构就是:
try
catch
finally基本结构:
try {
// 可能发生异常的代码
} catch (
Exception e) {
// 异常处理
} finally {
// 清理逻辑
}1.5.1 try
try 用来包裹:
可能发生异常的代码。
例如:
try {
System.out.println(
"第一行"
);
int result =
10 / 0;
System.out.println(
"第三行"
);
} catch (
ArithmeticException e) {
System.out.println(
"发生算术异常"
);
}输出:
第一行
发生算术异常发生异常以后:
System.out.println(
"第三行"
);不会继续正常执行。
执行流程:
进入 try
↓
正常执行代码
↓
发生异常
↓
try 后续代码停止
↓
寻找匹配的 catch1.5.2 catch
catch 用来:
捕获并处理 try 中抛出的异常。
例如:
try {
int result =
62 / 0;
} catch (
ArithmeticException e) {
System.out.println(
"除数不能为零"
);
}这里:
ArithmeticException e表示:
捕获 ArithmeticException
↓
异常对象保存到变量 e之后可以使用:
e.getMessage();
e.getCause();
e.printStackTrace();等方法获取异常信息。
1.5.3 多个 catch
一个:
try后面可以跟多个:
catch例如:
public static void test() {
try {
int[] array =
new int[8];
array[4] =
30 / 0;
System.out.println(
"try 块最后一行"
);
} catch (
ArithmeticException e) {
System.out.println(
"除数不能为零"
);
} catch (
ArrayIndexOutOfBoundsException e) {
System.out.println(
"数组越界"
);
} catch (
Exception e) {
System.out.println(
"其他异常"
);
}
System.out.println(
"try-catch 之后"
);
}这里:
30 / 0首先抛出:
ArithmeticException所以进入:
catch (
ArithmeticException e)如果改成:
array[9] =
30 / 1;就会抛出:
ArrayIndexOutOfBoundsException然后进入第二个:
catch1.5.4 catch 的顺序
如果异常之间存在继承关系:
Exception
↑
ArithmeticException那么:
更具体的子类异常必须写在前面。
正确:
try {
// ...
} catch (
ArithmeticException e) {
// ...
} catch (
Exception e) {
// ...
}不能反过来:
try {
// ...
// 编译错误
} catch (
Exception e) {
} catch (
ArithmeticException e) {
}因为:
Exception已经能够捕获:
ArithmeticException那么后面的:
catch (
ArithmeticException e)永远不会有机会执行。
所以:
多个 catch
↓
子类异常在前
↓
父类异常在后1.5.5 Multi-Catch
如果多个异常的处理逻辑完全一样,可以使用:
|同时捕获。
例如:
try {
int[] array =
new int[8];
array[9] =
30 / 1;
} catch (
ArithmeticException
| ArrayIndexOutOfBoundsException e) {
System.out.println(
"发生计算或数组异常"
);
}这种语法称为:
Multi-Catch相比:
catch (
ArithmeticException e) {
// 相同逻辑
}
catch (
ArrayIndexOutOfBoundsException e) {
// 相同逻辑
}可以减少重复代码。
1.5.6 finally
finally 通常用于:
无论正常执行还是发生异常,都希望执行的清理逻辑。
基本结构:
try {
// 业务代码
} catch (
Exception e) {
// 异常处理
} finally {
// 清理逻辑
}例如:
try {
System.out.println(
"执行业务"
);
} finally {
System.out.println(
"执行清理"
);
}通常情况下:
是否发生异常
↓
finally 都会执行finally 常用于:
释放资源
清理临时状态
关闭连接
执行收尾操作1.5.7 finally 与 return
例如:
public static int test() {
try {
return 1;
} finally {
System.out.println(
"finally"
);
}
}虽然:
try中已经执行:
return 1;但在方法真正返回之前,仍然会执行:
finally类似地:
break
continue通常也不会跳过 finally。
不过不要在:
finally中写:
return例如:
try {
return 1;
} finally {
return 2;
}因为它可能:
覆盖原来的返回值
吞掉原来的异常
让控制流程非常难理解所以:
finally 负责清理,不要负责改变程序的正常返回逻辑。
1.5.8 finally 不一定百分之百执行
平时可以理解:
finally 基本都会执行但它并不是物理意义上的:
100% 一定执行例如:
System.exit(0);会请求 JVM 直接结束。
这时:
finally可能没有机会执行。

另外:
JVM 崩溃
进程被强制杀死
kill -9
机器断电同样无法保证:
finally被执行。
所以更准确的说法是:
在 JVM 正常执行 Java 控制流程的情况下,finally 具有非常强的执行保证。
1.5.9 finally 与资源释放
过去:
finally经常用于关闭:
IO 流
Socket
数据库连接例如:
OutputStream output = null;
try {
output =
new FileOutputStream(
"filename"
);
// 写入数据
} finally {
if (output != null) {
output.close();
}
}这样可以尽量保证:
正常执行
或者
发生异常
↓
资源最终都能够关闭现代 Java 对实现了:
AutoCloseable的资源,一般更推荐:
try-with-resources例如:
try (
OutputStream output =
new FileOutputStream(
"filename"
)
) {
// 使用资源
}资源会自动关闭。
这部分后面在:
IO中再详细说明。
1.6 小结
1.6.1 Java 异常体系
Java 异常体系的根类:
Throwable主要分成:
Throwable
├── Error
└── Exception其中:
Error
↓
通常代表 JVM / 运行环境严重问题而:
Exception
↓
程序运行过程中需要处理的异常情况1.6.2 Checked 和 Unchecked
Checked Exception
↓
编译器要求处理
↓
catch 或 throws而:
RuntimeException
↓
Unchecked Exception
↓
编译器不强制处理1.6.3 异常处理核心关键字
try
↓
可能发生异常的代码
catch
↓
捕获并处理异常
finally
↓
执行清理逻辑
throw
↓
主动抛出异常对象
throws
↓
声明方法可能向外传播异常1.6.4 异常传播流程
异常发生
↓
当前正常流程被打断
↓
寻找匹配的 catch
↓
找到
↓
执行异常处理
没有找到
↓
继续向调用者传播
↓
一直传播到线程顶层
↓
仍然没人处理
↓
当前线程终止1.6.5 实际开发注意事项
实际开发中需要记住:
能真正处理再 catch
不能处理就继续抛
不要吞异常
不要丢失原始异常
日志要保留完整异常对象
更具体的 catch 放前面
能够提前校验的问题不要依赖异常控制流程
资源一定要正确释放例如转换异常时:
catch (
SQLException e) {
throw new RuntimeException(
"数据库操作失败",
e
);
}这里把:
e作为:
cause继续传递。
于是:
新的 RuntimeException
↓
仍然保留原始 SQLException
↓
完整异常调用链不会丢失生产环境记录日志时,也更推荐:
log.error(
"保存用户失败",
e
);而不是:
log.error(
e.getMessage()
);因为后者通常只有:
异常消息而没有完整:
Stack Trace一句话概括:
Java 异常机制的本质,就是让程序在出现非正常情况时,能够有结构地传播、定位和处理问题。
2. try-with-resources
try-with-resources 是 Java 7 引入的一种资源管理语法,主要用于自动关闭:
IO 流
Reader / Writer
Socket
数据库连接
以及其他需要手动释放的资源它解决的核心问题就是:
资源使用完成后,由 Java 自动调用
close()方法,不再需要手动在finally中关闭。
使用 try-with-resources 的前提是:
资源类型实现了 AutoCloseable或者它的子接口:
Closeable整体结构:
try (
// 需要自动关闭的资源
) {
// 使用资源
} catch (
Exception e) {
// 异常处理
}2.1 为什么需要 try-with-resources
在 Java 7 之前,通常使用:
try-catch-finally关闭资源。
例如读取文件:
public class TryCatchFinallyDecoder {
public static void main(
String[] args) {
BufferedReader reader = null;
try {
String path =
TryCatchFinallyDecoder.class
.getResource(
"/牛逼.txt"
)
.getFile();
String decodePath =
URLDecoder.decode(
path,
"UTF-8"
);
reader =
new BufferedReader(
new FileReader(
decodePath
)
);
String line;
while (
(line =
reader.readLine())
!= null) {
System.out.println(
line
);
}
} catch (
IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (
IOException e) {
e.printStackTrace();
}
}
}
}
}这里真正读取文件的代码其实并不复杂:
while (
(line = reader.readLine())
!= null) {
System.out.println(line);
}但为了关闭:
BufferedReader却需要写大量:
finally
null 判断
try-catch
close()整个结构变成:
创建资源
↓
try 使用
↓
catch 处理异常
↓
finally
↓
判断资源是否为 null
↓
再次 try-catch
↓
close()代码比较臃肿。
2.2 finally 关闭资源存在的问题
传统:
try-finally除了代码繁琐,还有一个比较重要的问题:
业务代码和关闭资源的代码都可能抛出异常。
例如定义:
class MyFinallyReadLineThrow {
public void readLine()
throws Exception {
throw new Exception(
"readLine"
);
}
public void close()
throws Exception {
throw new Exception(
"close"
);
}
}然后:
public static void main(
String[] args)
throws Exception {
MyFinallyReadLineThrow resource =
null;
try {
resource =
new MyFinallyReadLineThrow();
resource.readLine();
} finally {
resource.close();
}
}这里:
readLine()
↓
抛出异常
finally
↓
close()
↓
又抛出异常最终:
close() 的异常可能覆盖掉原本:
readLine() 的异常
这会导致:
真正首先发生的问题
↓
被资源关闭异常掩盖从而增加问题排查难度。
而:
try-with-resources专门处理了这种情况。
2.3 try-with-resources 基本使用
使用:
try-with-resources后,可以直接把资源声明放到:
try (...)中。
例如:
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
decodePath
)
)
) {
String line;
while (
(line =
reader.readLine())
!= null) {
System.out.println(
line
);
}
} catch (
IOException e) {
e.printStackTrace();
}这里已经不需要:
finally
reader.close()因为 Java 会自动调用:
reader.close();整个流程变成:
创建资源
↓
进入 try
↓
使用资源
↓
离开 try
↓
自动 close()无论:
正常执行结束
还是
try 中抛出异常都会尝试关闭资源。
2.4 AutoCloseable 接口
try-with-resources 能够自动关闭资源的关键就在:
AutoCloseable接口。
它的核心结构非常简单:
public interface AutoCloseable {
void close()
throws Exception;
}也就是说:
只要一个对象实现了
AutoCloseable,就可以放进 try-with-resources 中。
例如 Java 中大量资源类都满足这个条件:
BufferedReader
FileReader
InputStream
OutputStream
Connection
Statement
ResultSet其中很多 IO 类实现的是:
Closeable而:
Closeable本身又继承:
AutoCloseable关系可以简单理解:
AutoCloseable
↑
Closeable
↑
各种 IO 资源2.5 多个资源
try-with-resources 可以同时管理多个资源。
例如:
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
decodePath
)
);
PrintWriter writer =
new PrintWriter(
new File(
writePath
)
)
) {
String line;
while (
(line =
reader.readLine())
!= null) {
writer.println(line);
}
} catch (
IOException e) {
e.printStackTrace();
}多个资源之间使用:
;分隔。
例如:
try (
ResourceA a =
new ResourceA();
ResourceB b =
new ResourceB()
) {
}2.5.1 多个资源的关闭顺序
多个资源的关闭顺序和创建顺序相反。
例如:
try (
ResourceA a =
new ResourceA();
ResourceB b =
new ResourceB();
ResourceC c =
new ResourceC()
) {
}创建顺序:
A
↓
B
↓
C关闭顺序:
C
↓
B
↓
A也就是:
后创建的资源先关闭。
这种设计比较符合资源之间可能存在依赖关系的情况。
2.6 自定义资源
如果自己的类也需要支持:
try-with-resources只需要实现:
AutoCloseable即可。
例如:
class MyResource
implements AutoCloseable {
@Override
public void close()
throws Exception {
System.out.println(
"关闭自定义资源"
);
}
}使用:
public class TryWithResourcesCustom {
public static void main(
String[] args) {
try (
MyResource resource =
new MyResource()
) {
System.out.println(
"使用资源"
);
} catch (
Exception e) {
e.printStackTrace();
}
}
}输出:
使用资源
关闭自定义资源可以看到:
close()并没有手动调用。
Java 会在离开:
try作用域时自动调用。
2.7 自动调用 close()
再给:
MyResource增加一个方法:
class MyResource
implements AutoCloseable {
public void out()
throws Exception {
System.out.println(
"kerwim,Java 牛马"
);
}
@Override
public void close()
throws Exception {
System.out.println(
"关闭自定义资源"
);
}
}调用:
public static void main(
String[] args) {
try (
MyResource resource =
new MyResource()
) {
resource.out();
} catch (
Exception e) {
e.printStackTrace();
}
}输出:
kerwim,Java 牛马
关闭自定义资源整个过程:
new MyResource()
↓
resource.out()
↓
try 执行结束
↓
自动 resource.close()所以:
try-with-resources 本质上仍然会调用资源的
close(),只是这部分工作由编译器帮我们生成。
2.8 Suppressed Exception
try-with-resources 一个非常重要的能力就是:
Suppressed Exception
被抑制异常假设资源方法和关闭方法都会抛出异常:
class MyResourceOutThrow
implements AutoCloseable {
public void out()
throws Exception {
throw new Exception(
"out()"
);
}
@Override
public void close()
throws Exception {
throw new Exception(
"close()"
);
}
}调用:
public static void main(
String[] args) {
try (
MyResourceOutThrow resource =
new MyResourceOutThrow()
) {
resource.out();
} catch (
Exception e) {
e.printStackTrace();
}
}这里:
out()
↓
首先抛出异常
自动 close()
↓
再次抛出异常try-with-resources 不会让:
close()的异常覆盖:
out()的异常。
而是:
out() 异常
↓
作为主要异常
close() 异常
↓
作为 Suppressed Exception输出类似:
java.lang.Exception: out()
at ...
Suppressed: java.lang.Exception: close()
at ...所以:
主要异常
↓
正常保留
资源关闭异常
↓
作为 Suppressed 保存不会丢失异常信息。
2.9 addSuppressed() 和 getSuppressed()
Java 的:
Throwable提供了:
addSuppressed()用于记录被抑制的异常。
概念上类似:
primaryException
.addSuppressed(
closeException
);也可以通过:
getSuppressed()获取这些异常。
例如:
catch (
Exception e) {
Throwable[] suppressed =
e.getSuppressed();
for (
Throwable throwable :
suppressed
) {
System.out.println(
throwable.getMessage()
);
}
}如果:
out()和:
close()都抛出了异常,那么:
e代表主要异常。
而:
e.getSuppressed()可以得到关闭资源时产生的异常。
这就是 try-with-resources 相比传统:
try-finally更加可靠的重要原因之一。
2.10 编译器如何处理 try-with-resources
例如:
try (
MyResource resource =
new MyResource()
) {
resource.out();
}编译以后,Java 编译器会生成相应的资源关闭逻辑。
概念上可以理解成:
MyResource resource =
new MyResource();
Throwable primaryException =
null;
try {
resource.out();
} catch (
Throwable e) {
primaryException = e;
throw e;
} finally {
if (resource != null) {
if (primaryException != null) {
try {
resource.close();
} catch (
Throwable closeException) {
primaryException
.addSuppressed(
closeException
);
}
} else {
resource.close();
}
}
}真实生成的字节码和反编译代码可能会因:
JDK 版本
编译器版本
反编译工具而有所不同。
因此不需要死记反编译后的代码。
真正要理解的是:
try-with-resources
↓
自动 close()
↓
保留主要异常
↓
关闭异常作为 Suppressed2.11 Java 9 对 try-with-resources 的改进
Java 7 中通常需要在:
try (...)里面声明资源:
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
"data.txt"
)
)
) {
// 使用 reader
}Java 9 开始,如果一个变量已经在外面声明,并且属于:
final
或者
effectively final就可以直接放进:
try (...)中。
例如:
BufferedReader reader =
new BufferedReader(
new FileReader(
"data.txt"
)
);
try (reader) {
System.out.println(
reader.readLine()
);
}这里:
reader没有被重新赋值,因此属于:
effectively final可以直接使用。
相比 Java 7:
Java 7
↓
通常在 try() 中重新声明资源
Java 9+
↓
可以直接使用已有的 final /
effectively final 资源变量2.12 try-with-resources 与 finally
try-with-resources 并不意味着:
finally 被废弃了两者解决的问题不同。
try-with-resources:
主要用于
自动关闭资源finally:
用于执行通用的收尾逻辑例如:
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
"data.txt"
)
)
) {
// 使用资源
} catch (
IOException e) {
// 处理异常
} finally {
System.out.println(
"整个读取流程结束"
);
}这里:
reader.close()
↓
try-with-resources 自动负责
输出收尾日志
↓
finally 负责所以它们并不冲突。
2.13 使用原则
对于需要关闭的资源:
IO 流
Socket
JDBC 资源
AutoCloseable 对象一般优先考虑:
try-with-resources而不是手动:
try {
} finally {
resource.close();
}原因主要有:
代码更加简洁
不容易忘记关闭资源
多个资源管理更加方便
自动按照逆序关闭
异常信息更加完整
支持 Suppressed Exception尤其不要写成:
try {
// 使用资源
} finally {
resource.close();
}然后完全忽略:
close() 本身也可能失败的问题。
2.14 小结
try-with-resources 的核心目的就是:
自动管理需要关闭的资源。
基本写法:
try (
Resource resource =
new Resource()
) {
// 使用资源
}使用条件:
资源实现 AutoCloseable执行流程:
创建资源
↓
执行 try
↓
正常结束 / 发生异常
↓
自动调用 close()多个资源:
创建顺序
A → B → C
关闭顺序
C → B → A发生多个异常时:
业务异常
↓
作为主要异常
close() 异常
↓
作为 Suppressed Exception常用方法:
Throwable.getSuppressed()可以获取被抑制的异常。
Java 9 以后还可以直接使用:
final
或者
effectively final的已有资源变量。
最后可以简单记成:
传统方式
try
↓
使用资源
↓
finally
↓
手动 close()
try-with-resources
try (资源)
↓
使用资源
↓
Java 自动 close()一句话概括:
凡是实现了
AutoCloseable、使用结束后必须关闭的资源,优先考虑 try-with-resources。
3. 异常处理最佳实践
异常处理不是简单地:
哪里报错
↓
哪里 try-catch真正需要考虑的是:
异常是否应该发生
应该在哪一层抛出
应该在哪一层捕获
是否需要转换异常
是否需要记录日志
是否需要重试或补偿
是否需要释放资源下面整理一些实际开发中比较重要的异常处理原则。
3.1 不要用 catch 掩盖本可以提前避免的 RuntimeException
对于很多可以通过正常逻辑提前判断的问题,不应该依赖:
try-catch来控制流程。
例如:
String name = null;不要这样:
try {
System.out.println(
name.length()
);
} catch (
NullPointerException e) {
// ...
}如果:
null本身属于允许出现的状态,更合理的是提前判断:
if (name != null) {
System.out.println(
name.length()
);
}再例如:
if (index >= 0
&& index < list.size()) {
System.out.println(
list.get(index)
);
}而不是故意调用:
list.get(index);然后再捕获:
IndexOutOfBoundsException可以简单理解:
能够正常判断
↓
优先正常判断
真正的异常情况
↓
再交给异常机制不过也不能理解成:
RuntimeException 永远不能捕获。
例如:
Controller 全局异常处理
任务调度入口
线程执行边界
RPC / 消息消费边界这些边界层有时需要统一捕获:
RuntimeException进行:
日志记录
事务回滚
统一响应
任务隔离重点不是:
绝对不能 catch RuntimeException而是:
不要把本来应该通过正常代码避免的问题,全部交给 RuntimeException 来控制流程。
3.2 优先使用 try-with-resources 关闭资源
对于实现:
AutoCloseable的资源,优先使用:
try-with-resources例如:
try (
FileInputStream inputStream =
new FileInputStream(
"./tmp.txt"
)
) {
// 使用资源
} catch (
IOException e) {
log.error(
"读取文件失败",
e
);
}不要:
public void readFile() {
FileInputStream inputStream =
null;
try {
inputStream =
new FileInputStream(
"./tmp.txt"
);
// 使用资源
inputStream.close();
} catch (
IOException e) {
log.error(
"读取文件失败",
e
);
}
}问题在于:
资源创建成功
↓
业务代码发生异常
↓
还没执行到 close()
↓
资源没有关闭try-with-resources 会自动:
正常执行结束
或者
发生异常
↓
close()如果资源没有实现:
AutoCloseable才需要考虑自己在:
finally中完成释放。
3.3 一般不要直接捕获 Throwable
异常体系:
Throwable
├── Error
└── Exception如果直接:
try {
// ...
} catch (
Throwable throwable) {
// ...
}意味着连:
Exception和:
Error都会一起捕获。
例如:
OutOfMemoryError
StackOverflowError这类问题通常已经超出了普通业务代码能够恢复的范围。
所以一般不要:
catch (Throwable t)更合理的是根据职责捕获:
catch (IOException e)或者:
catch (RuntimeException e)等具体范围。
极少数基础设施代码可能会因为:
线程隔离
容器保护
框架边界需要捕获 Throwable,但这种代码应该非常谨慎,并且不能简单把:
Error吞掉。
3.4 捕获异常后不要丢失异常信息
非常危险的写法:
try {
// ...
} catch (
NumberFormatException e) {
}这里发生了:
异常
↓
被捕获
↓
什么都没做
↓
异常彻底消失通常称为:
吞异常如果确实需要记录:
try {
// ...
} catch (
NumberFormatException e) {
log.error(
"数字转换失败",
e
);
}不要只写:
log.error(
"数字转换失败:"
+ e
);更不要只写:
log.error(
e.getMessage()
);因为:
log.error(
"数字转换失败",
e
);可以保留完整:
异常类型
异常信息
Stack Trace
cause3.5 避免同一个异常在调用链中重复记录
例如底层:
try {
// ...
} catch (
NumberFormatException e) {
log.error(
"转换失败",
e
);
throw e;
}然后上层又:
catch (
NumberFormatException e) {
log.error(
"业务执行失败",
e
);
}最终同一个异常可能在日志中出现:
两次
三次
甚至更多次造成大量重复日志。
一般可以遵循:
谁最终处理异常,谁负责记录完整异常日志。
如果当前层只是负责:
转换异常可以:
try {
// ...
} catch (
NumberFormatException e) {
throw new MyBusinessException(
"数字格式错误",
e
);
}然后在真正的边界层统一记录。
例如:
DAO
↓
抛异常
Service
↓
转换业务异常
GlobalExceptionHandler
↓
统一记录日志当然这不是绝对规则。
如果当前层掌握了其他层无法获得的重要上下文,也可以记录必要信息,但应该避免:
同一个 Stack Trace在整个调用链反复打印。
3.6 不要在 finally 中 return
例如:
private int value;
public int test() {
try {
return ++value;
} finally {
return ++value;
}
}执行:
try
↓
准备返回 1
↓
执行 finally
↓
finally 又 return 2
↓
最终返回 2也就是说:
finally 中的 return会覆盖:
try 中原本的 return更加危险的是:
try {
throw new RuntimeException(
"业务异常"
);
} finally {
return;
}这甚至可能把原本的异常:
吞掉所以:
finally 应该负责清理,不应该负责改变方法的返回结果。
3.7 throws 尽量声明具体异常,而不是笼统 Exception
不推荐:
public void read()
throws Exception {
}因为调用者只知道:
可能有个 Exception却不知道具体:
文件问题?
数据库问题?
网络问题?如果明确会抛出:
IOException更推荐:
public void read()
throws IOException {
}调用者能够更加准确地决定:
怎么处理
是否重试
是否转换异常自定义业务 API 也可以通过一个明确的业务异常进行抽象:
public void createOrder()
throws OrderCreateException {
}重点在于:
方法声明的异常应该能够表达这个方法真正可能出现的问题。
3.8 catch 尽量捕获具体异常
例如:
try {
readFile();
} catch (
Exception e) {
log.error(
"执行失败",
e
);
}虽然方便,但:
Exception范围很大。
它可能同时捕获:
IOException
NullPointerException
IllegalArgumentException
ClassCastException
其他 RuntimeException这样会让完全不同的问题走进同一套处理逻辑。
如果业务知道可能出现:
FileNotFoundException
IOException可以更加具体:
try {
readFile();
} catch (
FileNotFoundException e) {
log.error(
"文件不存在",
e
);
} catch (
IOException e) {
log.error(
"文件读取失败",
e
);
}因为:
FileNotFoundException
↓
比 IOException 更具体所以必须放在:
IOException前面。
不过在系统边界层,例如:
统一异常处理器
线程入口
消息消费入口
任务调度入口使用:
catch (Exception e)进行最后兜底是合理的。
所以更准确的原则是:
业务处理中尽量捕获具体异常,边界层可以进行统一兜底。
3.9 自定义异常时必须保留原始 cause
不推荐:
catch (
NoSuchMethodException e) {
throw new MyServiceException(
"调用方法失败:"
+ e.getMessage()
);
}这里虽然生成了一个新的异常,但是:
原来的异常对象已经丢失。
更推荐:
catch (
NoSuchMethodException e) {
throw new MyServiceException(
"调用方法失败",
e
);
}自定义异常:
public class MyServiceException
extends RuntimeException {
public MyServiceException(
String message,
Throwable cause) {
super(
message,
cause
);
}
}这样:
MyServiceException
↓
cause
↓
NoSuchMethodException整个异常链都能够保留下来。
一般没有必要为了保留异常堆栈而重写:
printStackTrace()Java 自身的:
Throwable cause机制已经可以完整保存异常链。
3.10 finally 中不要让新的异常覆盖原始异常
例如:
try {
someMethod();
} finally {
cleanUp();
}假设:
someMethod()
↓
抛出 Exception A然后:
cleanUp()
↓
又抛出 Exception B传统:
try-finally下可能导致:
Exception B
↓
覆盖 Exception A真正首先发生的问题反而丢失。
所以资源关闭优先使用:
try-with-resources因为它会:
业务异常
↓
Primary Exception
关闭资源异常
↓
Suppressed Exception例如:
java.lang.Exception: business
Suppressed:
java.lang.Exception: close不会简单覆盖原始异常。
如果必须自己写:
finally也应该认真处理清理过程可能出现的异常,而不是让它随意覆盖原异常。
3.11 生产环境不要使用 printStackTrace()
学习和本地调试时:
e.printStackTrace();没有问题。
但生产环境不推荐。
因为:
printStackTrace()直接把异常堆栈输出到:
标准错误流
stderr它缺少正常日志系统具备的:
日志级别
文件滚动
结构化输出
时间信息
线程信息
链路信息
日志搜索
日志采集等能力。
生产代码更推荐:
try {
// ...
} catch (
Exception e) {
log.error(
"订单处理失败",
e
);
}常见日志体系:
SLF4J
Logback
Log4j2日志本身同样要注意:
密码
Token
身份证号
银行卡号
密钥等敏感信息不要直接输出。
3.12 不处理异常但需要清理时,可以使用 try-finally
假设:
method1();可能抛出异常。
当前层:
不准备处理这个异常但无论成功还是失败,都必须执行:
cleanUp();可以:
try {
method1();
} finally {
cleanUp();
}这里并不需要:
catch异常会继续向上传播。
执行流程:
method1()
↓
成功 / 异常
↓
finally
↓
cleanUp()
↓
异常继续向上传播不过如果:
cleanUp()只是为了关闭:
AutoCloseable资源,那么仍然应该优先考虑:
try-with-resources3.13 遵循“早 throw,晚 catch”
一个非常经典的异常设计原则:
早 throw
晚 catch3.13.1 早 throw
发现参数已经不合法,就尽早抛出。
例如:
public static int parseAge(
String value) {
if (value == null
|| value.isBlank()) {
throw new IllegalArgumentException(
"年龄不能为空"
);
}
return Integer.parseInt(
value
);
}而不是:
参数已经有问题
↓
继续执行很多业务逻辑
↓
更深层才发生异常越早发现错误:
异常位置越接近真正原因
系统状态越容易保持干净
问题越容易排查3.13.2 晚 catch
不要每一层都:
try-catch例如:
DAO
↓
Service
↓
Controller如果:
DAO 根本不知道怎么恢复没有必要立即捕获。
可以让异常传播到:
真正知道应该怎么处理的层例如:
Service
↓
转换业务异常
ControllerAdvice
↓
统一转换响应所以:
早 throw
↓
尽早发现非法状态
晚 catch
↓
在真正能够处理的位置捕获3.14 抛出的异常应该与当前抽象层相关
异常应该尽量表达:
当前方法到底发生了什么问题。
例如:
public User loadUser(
Long userId) {
}如果底层发生:
SQLExceptionService 层可能不希望把:
数据库实现细节直接暴露给更上层。
可以转换为:
throw new UserQueryException(
"查询用户失败",
e
);这样形成:
数据库层
↓
SQLException
业务层
↓
UserQueryException既保留:
cause又提高异常语义。
异常抽象应该和当前方法职责一致,而不是随便抛一个:
Exception或者与业务完全无关的异常。
3.15 不要使用异常处理正常业务流程
异常机制应该处理:
异常情况而不是普通分支判断。
例如:
try {
int age =
Integer.parseInt(
value
);
} catch (
NumberFormatException e) {
// 把它当普通 if 使用
}如果业务本来明确允许:
数字
非数字两种正常输入,并且可以低成本提前验证,就应该考虑普通控制结构:
if
switch
条件判断而不是把异常当成:
goto来使用。
例如业务只允许纯数字:
if (
value != null
&& value.matches("\\d+")
) {
int number =
Integer.parseInt(
value
);
}不过也不能机械理解成:
Integer.parseInt()
绝对不能捕获 NumberFormatException如果你调用的 API 本身就是通过异常表达解析失败,那么合理捕获:
NumberFormatException仍然没有问题。
原则是:
不要故意制造异常来代替正常、可预测的控制流程。
3.16 尽早完成校验,再执行有副作用的操作
例如用户注册需要:
验证用户
验证地址
保存用户
保存地址不推荐:
验证用户
↓
保存用户
↓
验证地址失败
↓
再想办法回滚如果可以提前完成校验,更合理:
验证用户
↓
验证地址
↓
校验全部通过
↓
保存用户
↓
保存地址这样可以减少:
无意义数据库写入
回滚次数
中间状态例如:
validateUserInput();
validateAddressInput();
saveUser();
saveAddress();不过:
提前校验不能替代数据库事务和数据库约束。
因为在:
校验完成和:
真正写入数据库之间,仍然可能发生:
并发修改
唯一键冲突
网络异常
数据库异常所以真正涉及多步数据库写操作时仍然需要:
事务保证一致性。
3.17 同一个异常尽量只记录一次完整堆栈
一个异常如果经过:
DAO
Service
Controller
全局异常处理器每一层都:
log.error(
"...",
e
);最终日志中会出现大量完全相同的:
Stack Trace真正的问题反而被日志噪音淹没。
例如:
DAO
↓
记录一次
Service
↓
再记录一次
Controller
↓
再记录一次
GlobalExceptionHandler
↓
再记录一次一个错误最终打印:
4 遍更好的方式通常是:
中间层
↓
补充上下文 / 转换异常
↓
继续抛
边界层
↓
统一记录完整异常另外,同一个事件的日志信息尽量集中表达。
例如:
log.debug(
"使用缓存区域 A,失败后重试区域 B"
);比把紧密相关的信息拆成很多零散日志更容易检索。
3.18 异常中要包含足够的上下文
只有:
保存失败通常没有太大价值。
更好的异常信息应该帮助开发人员回答:
什么操作失败?
哪个业务对象失败?
关键参数是什么?
失败发生在哪个阶段?例如:
throw new OrderException(
"订单支付失败,orderId="
+ orderId,
e
);日志:
log.error(
"读取配置文件失败,file={}",
filePath,
e
);好的异常信息通常包含:
业务动作
关键标识
必要上下文
Throwable cause但也不要把敏感信息直接写入异常:
密码
Token
Secret Key
银行卡号
完整身份证信息异常信息应该做到:
足够排查
但不过度泄露3.19 不要吞掉 InterruptedException
线程代码中:
InterruptedException非常特殊。
不要这样:
while (true) {
try {
Thread.sleep(
100000
);
} catch (
InterruptedException e) {
}
doSomething();
}这里:
线程已经收到中断请求
↓
却被 catch 吞掉
↓
线程继续运行可能导致:
线程池无法正常关闭
任务无法取消
应用停止变慢如果当前方法不能继续向外:
throws InterruptedException通常应该恢复中断状态:
catch (
InterruptedException e) {
Thread.currentThread()
.interrupt();
return;
}或者:
catch (
InterruptedException e) {
Thread.currentThread()
.interrupt();
break;
}为什么还要:
Thread.currentThread()
.interrupt();因为:
InterruptedException 被抛出时
↓
线程的中断标记通常会被清除重新调用:
interrupt()可以把中断状态恢复,让上层代码仍然能够感知:
这个线程曾经被要求中断因此可以记:
能继续抛
InterruptedException就继续抛;不能抛时,通常恢复中断状态并尽快结束当前任务。
3.20 重复异常处理逻辑应该统一封装
如果很多地方都有:
try {
// ...
} catch (
Exception e) {
// 完全相同处理
}或者:
try {
// ...
} finally {
close();
}说明公共逻辑可能应该抽取。
例如旧代码中经常出现:
public static void closeConnection(
Connection connection) {
try {
if (connection != null) {
connection.close();
}
} catch (
SQLException e) {
log.error(
"数据库连接关闭失败",
e
);
}
}然后统一调用:
finally {
DBUtil.closeConnection(
connection
);
}现代 Java 中,如果资源实现:
AutoCloseable应该优先:
try (
Connection connection =
getConnection()
) {
// 数据库操作
}实际项目中,还可以根据场景使用:
公共工具方法
模板方法
AOP
统一异常处理器
Spring @ControllerAdvice
事务模板
重试模板来减少重复的:
try-catch代码。
核心原则是:
公共异常处理属于基础设施逻辑时,不要散落在每一段业务代码里重复实现。
3.21 小结
异常处理最重要的不是:
try-catch 写得越多越安全而是:
异常在正确的位置产生
异常在正确的位置传播
异常在正确的位置处理
异常信息没有丢失
公共处理逻辑没有重复可以把前面的原则浓缩为:
可以提前判断的错误
↓
不要依赖异常控制流程
需要关闭的资源
↓
优先 try-with-resources
捕获异常
↓
尽量捕获具体类型
无法处理
↓
继续向上传播
转换异常
↓
保留原始 cause
记录日志
↓
保留完整 Stack Trace
同一个异常
↓
避免在调用链重复打印
finally
↓
不要 return
不要覆盖原异常
InterruptedException
↓
不要吞
恢复中断状态或继续抛出
业务输入
↓
尽早校验
数据库操作
↓
校验不能替代事务
重复异常逻辑
↓
统一封装最终可以记住一句:
早发现、早抛出;能处理再捕获;不能处理就传播;转换异常保留 cause;最终在合适的边界统一记录和处理。