Java 异常 - 实战注意点
实战注意点概览
-
异常的使用,是否可以做普通的循环控制
-
从“如果 catch 中有 return,finally 仍会在 return 之前执行;为什么不推荐catch 和 finally 都有 return”案例出发,说明其与return的执行过程
这里举了很多示例关于try - catch - try 与 return 的执行过程
-
详细解释try-with-resources
一、异常的使用,是否可以做普通的循环控制
只在真正异常时使用,不要用异常做普通的循环控制。这句话是异常使用的一条黄金准则,很多新手朋友容易踩坑。下面用具体代码对比 反例(用异常做循环控制) 和 正例(用正常逻辑判断),并解释为什么反例不好。
1. 反例:用异常来控制循环结束(错误❌)
场景:遍历数组,遇到负数就停止
int[] arr = {1, 2, 3, -1, 4, 5};
// ❌ 错误做法:用异常判断越界来终止循环
try {
for (int i = 0; ; i++) { // 没有终止条件
if (arr[i] < 0) {
// 假装抛一个异常来退出循环
throw new ArrayIndexOutOfBoundsException();
}
System.out.println(arr[i]);
}
} catch (ArrayIndexOutOfBoundsException e) {
// 捕获异常,循环结束
System.out.println("遇到负数或数组结束,停止");
}
输出:
1
2
3
遇到负数或数组结束,停止
为什么这是反例?
-
性能极差
- 每次抛出异常都要创建异常对象,并填充整个调用栈(
fillInStackTrace()非常耗时)。 - 对于只有几次迭代的小数组或许感觉不到,但如果循环次数很多(比如上万次),性能差距可能是 几十倍甚至上百倍。
- 每次抛出异常都要创建异常对象,并填充整个调用栈(
-
代码可读性差
- 看代码的人需要理解“为什么故意抛异常来退出循环”,完全违背直觉。
- 正常的循环终止条件(
i < arr.length和break)一目了然,异常机制不是用来做流程控制的。
-
隐藏的逻辑错误
- 如果数组中间有真正的越界 bug(比如数组长度 5 但访问下标 10),异常也会被这个
catch吞掉,导致你无法发现真实问题。
- 如果数组中间有真正的越界 bug(比如数组长度 5 但访问下标 10),异常也会被这个
-
违反异常设计初衷
- 异常应该表示 意料之外、不正常 的情况(比如文件突然不存在、网络断开)。
- 循环结束是 正常、预期 的行为,不应该用异常来表达。
2. 正例:用正常的条件判断控制循环(正确✅)
同一场景的正确写法
int[] arr = {1, 2, 3, -1, 4, 5};
// ✅ 正确做法:用条件判断
for (int i = 0; i < arr.length; i++) {
if (arr[i] < 0) {
System.out.println("遇到负数,停止");
break; // 正常退出循环
}
System.out.println(arr[i]);
}
输出:
1
2
3
遇到负数,停止
为什么这是正例?
- 性能好:只有正常的整数比较和分支跳转,没有异常对象创建和栈展开开销。
- 意图清晰:
i < arr.length控制边界,if (arr[i] < 0) break控制提前退出,任何人一眼看懂。 - 不会掩盖 bug:如果数组越界(比如误写成
arr[i+1]),JVM 会抛出ArrayIndexOutOfBoundsException且不会被这个break吞掉,我们能看到真实错误。
3. 另一个常见反例:用异常判断字符串是否是数字
// ❌ 反例:用异常判断是否为整数
public static boolean isInteger(String str) {
try {
Integer.parseInt(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
为什么不好?
- 只要输入不是数字,
parseInt就会抛出异常,而“非数字”在业务中可能是 很常见 的情况(比如用户输入了字母)。 - 频繁的异常创建和捕获会严重拖慢性能,尤其在高频调用的场景(如解析几十万行 CSV)。
✅ 正例:用正则或字符遍历判断
public static boolean isInteger(String str) {
if (str == null || str.isEmpty()) return false;
for (int i = 0; i < str.length(); i++) {
if (!Character.isDigit(str.charAt(i))) {
return false;
}
}
return true;
}
或者用正则:str.matches("\\d+")。
- 没有异常开销,逻辑清晰。
注意:如果绝大多数输入都是合法数字,只有极少数非法(比如配置文件解析,99.9% 都是正确格式),那么用
try-catch其实也可以接受,因为它属于“真正异常情况”。
但如果是用户频繁输入错误这种常见场景,一定不要用异常。
4. 什么时候才是“真正异常”?
真正异常 指:
- 发生频率极低。
- 表示一种错误状态,而不是预期的业务分支。
✅ 适合用异常的例子:
- 文件突然被删除(
FileNotFoundException)。 - 数据库连接突然断开(
SQLException)。 - 网络请求超时。
- 程序内部出现了不该发生的状态(比如 switch 走了 default,按逻辑绝不该发生)。
❌ 不适合用异常的例子:
- 用户输入错误(太常见)。
- 遍历结束判断。
- 空集合取元素时的检查。
- 类型转换判断(
instanceof能解决)。
5. 总结口诀
异常处理放意外,正常流程用判断。
性能可读两不误,莫把异常当 if 用。
二、如果 catch 中有 return,finally 仍会在 return 之前执行;为什么不推荐catch 和 finally 都有 return
这句话得详细举例说明,而且还会举一反三
这正是 finally 最容易让新手朋友困惑的地方之一。下面我用多个具体例子,详细演示 catch 中有 return 时 finally 的执行时机,并扩展到 try 中有 return、finally 中有 return 等常见变体。
1. 基础示例:catch 中的 return 和 finally
public class FinallyReturnDemo {
public static void main(String[] args) {
System.out.println("结果:" + test());
}
public static int test() {
try {
int a = 10 / 0; // 触发异常,跳转到 catch
return 1; // 不会执行
} catch (ArithmeticException e) {
System.out.println("catch 块");
return 2; // catch 中有 return
} finally {
System.out.println("finally 块");
}
}
}
输出:
catch 块
finally 块
结果:2
关键点:
catch中的return 2并没有立即结束方法,而是先转去执行finally块。finally执行完毕后,才真正return 2。- 返回值已经在
catch中确定了(值为2),finally无法改变这个值(除非finally中也return,后面会讲)。
2. 更精细的例子:验证返回值是否可被 finally 修改
如果 finally 中修改了准备返回的变量,会影响返回值吗?分情况:
情况1:返回值是基本类型(int、boolean等)
public static int test() {
int result = 0;
try {
int a = 10 / 0;
return result; // 这里不会执行
} catch (Exception e) {
result = 2;
return result; // 返回 result 的当前值(2)
} finally {
result = 100; // 修改 result
System.out.println("finally 执行,result = " + result);
}
}
输出:
finally 执行,result = 100
返回值:2
解释:
catch中的return result;执行时,会将result的 当前值(2) 存入一个临时变量(可以理解为“待返回的值”)。- 然后执行
finally,此时result被改为100,但 临时保存的那个返回值仍是2。 finally结束后,返回那个临时变量2。
情况2:返回值是引用类型(对象),修改对象内部属性
class Money {
int amount;
Money(int amount) { this.amount = amount; }
}
public static Money test() {
Money m = new Money(10);
try {
int a = 10 / 0;
return m;
} catch (Exception e) {
m.amount = 20;
return m;
} finally {
m.amount = 100; // 修改对象内部属性
System.out.println("finally 修改 amount = " + m.amount);
}
}
输出:
finally 修改 amount = 100
返回的 Money.amount = 100
解释:
catch中的return m;时,会保存对象引用(指向Money对象的地址)。finally中通过这个引用修改了对象内部的amount字段。- 因为引用没变,返回的还是同一个对象,所以修改有效。
总结:
finally无法改变 返回值本身(基本类型的值、引用变量的指向),但可以改变 引用对象内部的状态。
3. 特殊情况:finally 中也有 return
如果 finally 块中也含有 return,那么 finally 的返回值会覆盖 try/catch 的返回值,并且 异常也会被吞掉(非常危险,应该避免)。
public static int test() {
try {
int a = 10 / 0;
return 1;
} catch (ArithmeticException e) {
System.out.println("catch 返回 2");
return 2;
} finally {
System.out.println("finally 返回 3");
return 3; // 覆盖
}
}
输出:
catch 返回 2
finally 返回 3
返回值:3
注意:catch 中的 return 2 没有生效,异常信息也丢失了(因为 return 提前结束方法)。这会导致调用者无法得知发生了异常,强烈不推荐。
4. try 块中有 return 且没有异常
finally 同样会在 return 之前执行:
public static int test() {
try {
System.out.println("try 块");
return 1;
} finally {
System.out.println("finally 块");
}
}
输出:
try 块
finally 块
返回值:1
5. finally 执行时机的完整流程图
进入 try 块
|
+-- 发生异常 → 执行 catch 块(如果有)
| |
| +-- catch 中有 return → 暂存返回值 → 执行 finally → 返回暂存值
| +-- catch 无 return → 执行 finally → 继续后续(或向上抛异常)
|
+-- 无异常 → try 中有 return → 暂存返回值 → 执行 finally → 返回暂存值
或 try 无 return → 执行 finally → 继续后续
结论:无论 try 或 catch 中是否有 return、throw,finally 都会在方法真正退出之前执行(除了 System.exit(0) 或 JVM 崩溃)。
6. 实战中容易犯的错误(反例)
public static int readNumberFromFile(String filePath) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(filePath));
return Integer.parseInt(reader.readLine());
} catch (IOException e) {
e.printStackTrace();
return -1; // 返回默认值
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException e) {}
}
// 千万不能在 finally 中 return
// return -999; // 这会覆盖上面的 -1 和异常信息
}
}
正确做法:finally 只做清理,不要有 return。
7. 举一反三:finally 执行时机总结表
| 场景 | finally 是否执行 |
最终返回值来源 |
|---|---|---|
try 正常执行,无 return |
是 | try 之后的代码 |
try 正常执行,有 return |
是(在 return 前) |
try 中的 return 暂存值 |
try 异常,被 catch,catch 无 return |
是 | catch 之后的代码 |
try 异常,被 catch,catch 有 return |
是(在 return 前) |
catch 中的 return 暂存值 |
finally 中有 return |
是 | finally 中的 return 值(覆盖其他) |
try 或 catch 中有 System.exit(0) |
否 | 程序退出 |
8. 面试常见追问
问:finally 块在 catch 的 return 之前执行,那如果 finally 里也抛了异常怎么办?
答:finally 中的异常会传播出去,覆盖 try/catch 中原有的异常(类似 finally 中的 return 会覆盖返回值)。所以 finally 中应避免抛出异常,或者在 finally 内部捕获并处理。
问:try 中有 return,finally 修改了返回值对象的内容,返回值会变吗?
答:如果返回值是基本类型或不可变对象(如 String、Integer),不会变;如果返回值是可变对象的引用,对象内部可能被修改。
三、详细解释try-with-resources
1. try-with-resources 是什么?
try-with-resources 是 Java 7 引入的一个语法特性,用于自动管理资源。
资源指那些必须手动关闭的对象(如文件流、数据库连接、网络连接等),它们通常实现了 java.lang.AutoCloseable 接口(或其子接口 java.io.Closeable)。
传统写法:在 finally 块中手动调用 close(),代码冗长且容易忘记关闭。
try-with-resources:将资源声明在 try 关键字后的括号内,Java 会在 try 块执行完毕后自动调用资源的 close() 方法,无论是否发生异常。
2. 语法格式
try (ResourceType resource = new ResourceType(...)) {
// 使用资源
} catch (Exception e) {
// 处理异常(可选)
}
// 不需要 finally 来关闭资源
- 可以声明一个或多个资源,用分号
;分隔。 - 资源必须实现
AutoCloseable接口。 try块结束时,资源会按照声明的逆序自动关闭(后声明的先关闭)。- 可以同时使用
catch和finally,但通常finally不再需要(因为自动关闭)。
3. 工作原理(底层机制)
- 编译时脱糖:编译器将
try-with-resources转换为传统的try-catch-finally代码,自动生成关闭资源的逻辑。 - 异常抑制:如果
try块和资源close()方法都抛出异常,try块中的异常是主异常,close()的异常会被添加到主异常的 抑制异常列表 中,可通过Throwable.getSuppressed()获取。 - 关闭顺序:多个资源时,按声明逆序关闭(后声明的先关闭),避免依赖问题。
4. 完整 Demo
Demo 1:单个资源(读取文件)
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesDemo1 {
public static void main(String[] args) {
// try-with-resources 自动关闭 BufferedReader
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("读取文件出错:" + e.getMessage());
}
// 无需 finally 关闭 br
}
}
对比传统写法(没有 try-with-resources):
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("input.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
可见 try-with-resources 大大简化了代码。
Demo 2:多个资源(同时操作输入和输出流)
import java.io.*;
public class TryWithResourcesDemo2 {
public static void main(String[] args) {
// 同时声明两个资源:输入流和输出流
try (BufferedReader reader = new BufferedReader(new FileReader("source.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("dest.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
System.out.println("复制完成!");
} catch (IOException e) {
System.err.println("文件操作失败:" + e.getMessage());
}
// reader 和 writer 会自动关闭(writer 先关闭,然后 reader)
}
}
关闭顺序:先关闭 writer,再关闭 reader(与声明顺序相反)。
Demo 3:自定义资源(实现 AutoCloseable)
任何实现了 AutoCloseable 的类都可以用于 try-with-resources。
例如上述
BufferedReader 是实现了接口 AutoCloseable 的
class MyResource implements AutoCloseable {
private String name;
public MyResource(String name) {
this.name = name;
System.out.println(name + " 资源已打开");
}
public void doWork() {
System.out.println(name + " 正在工作...");
// 模拟可能抛出的异常
if (name.equals("Resource-B")) {
throw new RuntimeException(name + " 工作出错");
}
}
@Override
public void close() throws Exception {
System.out.println(name + " 资源已关闭");
// 模拟关闭时抛出的异常
if (name.equals("Resource-A")) {
throw new Exception(name + " 关闭时发生异常");
}
}
}
public class TryWithResourcesDemo3 {
public static void main(String[] args) {
try (MyResource resA = new MyResource("Resource-A");
MyResource resB = new MyResource("Resource-B")) {
resA.doWork(); // 正常
resB.doWork(); // 这里会抛出 RuntimeException
} catch (Exception e) {
System.err.println("捕获到异常:" + e.getMessage());
// 获取被抑制的异常(来自 close 方法)
Throwable[] suppressed = e.getSuppressed();
for (Throwable t : suppressed) {
System.err.println("抑制的异常:" + t.getMessage());
}
}
}
}
输出(可能顺序略有不同,但逻辑一致):
Resource-A 资源已打开
Resource-B 资源已打开
Resource-A 正在工作...
Resource-B 正在工作...
Resource-B 资源已关闭
Resource-A 资源已关闭
捕获到异常:Resource-B 工作出错
抑制的异常:Resource-A 关闭时发生异常
解释:
resB.doWork()抛出RuntimeException,这是主异常。- 自动关闭时,先关
resB(正常关闭),再关resA(close()抛出异常)。 resA关闭时的异常被抑制,附加到主异常的抑制列表中。- 最终
catch捕获主异常,可通过getSuppressed()查看被抑制的异常。
5. 异常抑制详解
- 主异常:
try块中抛出的第一个异常(工作逻辑中的异常)。 - 抑制异常:在自动关闭资源时,
close()方法抛出的异常。它们不会覆盖主异常,而是被附加到主异常上。 - 目的:保留最根本的业务异常,同时不丢失资源关闭时的异常信息。
6. 注意事项
-
资源变量默认是
final或 effectively final
在try-with-resources括号内声明的资源变量是隐式final,不能在try块中重新赋值。try (BufferedReader br = new BufferedReader(...)) { br = new BufferedReader(...); // 编译错误:不能重新赋值 } -
可以同时使用
catch和finally
即使资源自动关闭,你仍可以添加catch处理异常,或者添加finally执行额外清理(虽然很少需要)。 -
Java 9 改进:可以使用 effectively final 的变量
如果资源变量已经在外部声明且是 effectively final,可以直接在try括号内使用。BufferedReader br = new BufferedReader(new FileReader("file.txt")); try (br) { // Java 9+ 允许 // 使用 br } -
不要忘记处理异常
资源close()方法声明抛出Exception,建议在try-with-resources后使用catch捕获,或者方法上throws。
7. 总结对比
| 特性 | 传统 try-catch-finally | try-with-resources |
|---|---|---|
| 代码量 | 冗长,容易忘记关闭资源 | 简洁,自动关闭 |
| 异常处理 | 需要手动处理 close() 的异常 |
自动抑制 close() 异常 |
| 关闭顺序 | 手动控制,容易出错 | 自动逆序关闭,安全 |
| 是否推荐 | 仅兼容旧版本 Java 6 及以下 | Java 7+ 官方推荐 |
结论:只要资源实现了 AutoCloseable,就应该优先使用 try-with-resources,这是现代 Java 的最佳实践。
更多推荐




所有评论(0)