摘要 :
try里有return,finally里也有return,最终返回哪个?答案是finally的。更可怕的是,finally里的return会吞掉try里的异常。
一、问题现象
java
public class TryFinallyTest {
public static void main(String[] args) {
System.out.println(test()); // 输出什么?
}
public static String test() {
try {
System.out.println("try");
return "try的返回值";
} finally {
System.out.println("finally");
return "finally的返回值"; // ❌ 覆盖了 try 的 return
}
}
}
运行结果:
try
finally
finally的返回值
try 里的 return 被 finally 里的 return 覆盖了!
再看一个更隐蔽的:
java
public static void main(String[] args) {
System.out.println(test2()); // 输出什么?
}
public static String test2() {
try {
System.out.println("try");
throw new RuntimeException("try里的异常");
} finally {
System.out.println("finally");
return "finally吞掉了异常"; // ❌ 异常被吞了!
}
}
运行结果:
try
finally
finally吞掉了异常
RuntimeException 被 finally 的 return 静默吞掉了!
二、踩坑现场
场景 1:finally 里修改返回值
java
// ❌ 常见错误认知:认为 finally 里对返回值的修改会生效
public static int test() {
int result = 0;
try {
result = 1;
return result;
} finally {
result = 2; // 对返回值的修改不生效!
}
}
System.out.println(test()); // 1(不是 2)
为什么 :return result 在 finally 执行前已经把 result 的值(1)保存到了返回值槽位 ,finally 里修改的是局部变量,不影响已经保存的返回值。
例外 :如果返回的是引用类型,且修改的是对象的内容(不是重新赋值),则会生效。
java
public static List<String> test() {
List<String> list = new ArrayList<>();
try {
list.add("try");
return list;
} finally {
list.add("finally"); // ✅ 修改对象内容,会影响返回值
}
}
// 返回 ["try", "finally"]
场景 2:finally 里的 return 吞异常
java
// ❌ 生产代码里最危险的写法
public void processOrder(Long orderId) {
try {
orderService.validate(orderId);
orderService.process(orderId);
} finally {
// 清理资源
lockService.unlock(orderId);
return; // ❌ 如果这里写成 return,try 里的异常会被吞掉!
}
}
三、原理解析
3.1 JVM 字节码视角
try-finally 在字节码层面是通过**异常表(Exception Table)**实现的。
java
// 你写的代码
try {
return 1;
} finally {
System.out.println("finally");
}
字节码等价于(概念上):
java
try {
int result = 1;
finallyBlock(); // finally 在 return 前执行
return result;
} catch (Throwable e) {
finallyBlock();
throw e;
}
关键规则 :finally 块会在 try 的 return 之前 执行,但如果 finally 里也有 return,它会覆盖 try 的返回值。
3.2 返回值保存机制
try { return x; }
│
▼
先把 x 的值保存到"返回值槽位"(局部变量表的一个 slot)
│
▼
执行 finally 块
│
├── finally 没有 return → 返回"返回值槽位"里的值
└── finally 有 return y → 返回 y(覆盖)
3.3 异常被吞的字节码原理
finally 块里的 return 会导致方法正常返回,JVM 不会再向外抛异常。
try { throw new RuntimeException(); }
│
▼
执行 finally 块
│
├── finally 没有 return → 异常继续向外抛 ✅
└── finally 有 return → 方法正常返回,异常被丢弃 ❌
四、正确写法
4.1 finally 里不要写 return
java
// ❌ 错误
public static String test() {
try {
return "try";
} finally {
return "finally"; // 禁止!
}
}
// ✅ 正确:finally 只做清理,不返回值
public static String test() {
try {
return "try";
} finally {
System.out.println("清理资源");
// 没有 return
}
}
4.2 用 try-with-resources(Java 7+)
java
// ✅ 推荐:自动关闭资源,不需要手动写 finally
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
return br.readLine();
} catch (IOException e) {
log.error("读取文件失败", e);
return null;
}
// 不需要 finally,资源自动关闭
4.3 finally 里只做资源释放,不写业务逻辑
java
// ✅ 正确写法
public void process(Long id) {
Lock lock = lockService.getLock(id);
lock.lock();
try {
// 业务逻辑
doProcess(id);
} finally {
// 只做资源释放
lock.unlock();
}
}
4.4 如果 finally 里可能抛异常,要单独 try-catch
java
// ✅ 正确:保证资源清理的异常不影响主流程的异常
public void process() {
Lock lock = lockService.getLock();
lock.lock();
try {
doProcess();
} finally {
try {
lock.unlock(); // unlock 也可能抛异常
} catch (Exception e) {
log.warn("释放锁失败", e);
}
}
}
五、最佳实践
✅ finally 块的 3 条铁律
- finally 里永远不要写
return(IDEA 会直接报警告) - finally 里只做资源释放,不写业务逻辑
- finally 里抛出的异常会覆盖 try 里的异常(要尽量处理掉)
🔍 字节码验证:返回值究竟怎么保存的
可以用 javap -c 查看编译后的字节码:
bash
javac Test.java
javap -c Test.class
你会看到 astore/iload 等指令在 finally 之前保存返回值。
🛠️ IDEA inspections 开启
finally' block does not complete normally→ 警告:finally 块里有 return/throw'return' inside 'finally' block→ 错误:finally 里的 return
六、小结
finally在try的return之前执行finally里有return→ 覆盖try的返回值finally里有return→ 吞掉try里抛出的异常(最危险)- finally 块的正确用途只有一种:释放资源
- Java 7+ 优先用 try-with-resources,避免手写 finally
- IDEA 里 finally 写 return 会标黄,千万不要忽略这个警告
下一篇预告:泛型擦除:为什么 List<String> 和 List<Integer> 是一家人? ------ 泛型只在编译期存在,运行期被擦除了,这个设计带来了哪些坑?