finally 与 return 执行顺序
面试官问:"try-catch-finally 中,finally 和 return 哪个先执行?"
候选人小周答:"finally 先执行,然后 return。"
面试官写了一段代码:
public int test() {
try {
return 1;
} finally {
System.out.println("finally");
}
}
"输出什么?"
小周:"1 和 finally。"
面试官又写了一段:
public int test() {
int x = 1;
try {
return x;
} finally {
x = 2;
}
}
"返回什么?"
小周:"1?"
面试官:"还是 1。"
小周困惑了:"finally 不是执行了吗?x 不是改成 2 了吗?"
【面试官心理】
这道题能精准区分"背过结论"和"真正理解过字节码"的候选人。知道"JVM 保存返回值副本"的人,才能真正理解为什么 finally 修改变量无效。
一、finally 的执行时机 🔴
1.1 正常流程
finally 总是在 try 或 catch 的 return 之前执行:
public int test() {
try {
System.out.println("try");
return 1;
} finally {
System.out.println("finally"); // 先执行
}
// return 1; // 实际 return 在这里,finally 之后
}
输出:try → finally
1.2 return 值覆盖原理
回到小周的困惑:
public int test() {
int x = 1;
try {
return x; // JVM 将 x 的值(1)保存到"返回值寄存器"
} finally {
x = 2; // 修改的是 x,但返回值已经保存了
}
// return 1; // 返回的是保存的值,而不是 x 的当前值
}
字节码层面的真相:
iload_1 // 将 x (1) 压入操作数栈
istore_2 // 将栈顶值(1)保存到"返回值槽"(局部变量表 slot 2)
// try 块结束
// 执行 finally
iconst_2 // 将 2 压入栈
istore_1 // 将 2 保存到 x(局部变量表 slot 1)
iload_2 // 从返回值槽加载返回值(1)← 关键!
ireturn // 返回
finally 修改的是局部变量 x,但返回值在 try 块 return 之前就已经保存了。
⚠️
finally 中修改变量不会影响 return 值,因为 return 值在 try 块执行时就被保存了。如果想通过 finally 修改返回值,应该用数组或对象包装。
二、finally 不一定执行的场景 🔴
2.1 System.exit()
try {
System.out.println("try");
System.exit(0); // JVM 直接退出
} finally {
System.out.println("finally"); // 不会执行!
}
System.exit() 会终止 JVM 进程,finally 根本没机会执行。
2.2 OOM/StackOverflow 导致 JVM 崩溃
try {
// 触发 StackOverflowError
recursiveMethod();
} finally {
System.out.println("finally"); // 可能不会执行
}
当 StackOverflowError 发生时,JVM 的栈空间已经严重不足,执行 finally 可能再次触发 StackOverflowError,导致 finally 无法完成。
2.3 守护线程中的 finally
Thread daemon = new Thread(() -> {
try {
// ...
} finally {
System.out.println("daemon finally"); // 如果主线程退出,JVM 直接退出
}
});
daemon.setDaemon(true);
daemon.start();
当所有非守护线程结束后,JVM 会直接退出,守护线程的 finally 可能不会执行。
2.4 CPU 断电/硬件故障
这是最极端的情况,但理论上 JVM 进程被强制终止时,finally 不执行。
三、return 值类型的影响 🔴
3.1 返回对象引用时
public StringBuilder test() {
StringBuilder sb = new StringBuilder("hello");
try {
return sb; // 保存的是引用(指向 sb 对象)
} finally {
sb.append(" world"); // 通过引用修改了对象内容
// 但 return 的是同一个引用
}
}
// 返回值:"hello world"
finally 修改的是对象的内部状态(通过引用),而不是引用本身。return 的引用指向的对象已经被修改了。
💡
这个区别非常关键:基本类型修改返回值不影响(传值);引用类型修改内部状态会影响返回结果(传引用)。
3.2 try-finally 没有 catch
public int test() {
try {
return 1;
} finally {
System.out.println("finally");
}
}
// 编译通过,finally 正常执行
// 如果 try 中有异常,finally 也会执行
四、追问升级
面试官:"finally 中有 return 会怎样?"
public int test() {
try {
return 1;
} finally {
return 2; // finally 中的 return!
}
}
// 编译通过,返回值:2(覆盖了 try 的 return)
这是极其糟糕的写法!finally 中的 return 会覆盖 try 中的 return,且 try 中的异常也会被静默丢弃:
public int test() {
try {
throw new RuntimeException("error"); // 异常被丢弃
} finally {
return 0; // 正常返回,无异常
}
}
// 调用方:返回 0,没有异常抛出!
⚠️
禁止在 finally 中写 return 语句。这是 Java 代码规范的铁律。finally 中的 return 会静默丢弃 try 中的异常,是极其隐蔽的 bug 来源。
面试官:"try 中 return,catch 中也 return,finally 还执行吗?"
public int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.println("finally"); // 仍然执行!
}
}
// finally 先执行,然后根据是否有异常/哪个分支,返回对应值
finally 总是在 return 之前执行,不管 return 在 try 还是 catch 中。
【面试官心理】
能说出"finally 中 return 丢弃异常"的候选人,是真正踩过坑或者读过 Effective Java 的。Effective Java 明确反对这种写法,这是 P6+ 的加分点。
五、生产避坑
5.1 常见错误:用 finally 做资源清理
// ❌ 错误写法:finally 中的异常会覆盖 try 中的异常
try {
doSomething();
} finally {
cleanup(); // 如果 cleanup 抛异常,doSomething 的异常就丢失了
}
// ✅ 正确写法:嵌套 try
try {
try {
doSomething();
} finally {
cleanupQuietly(); // 只记录,不抛异常
}
} catch (Exception e) {
handleError(e);
}
5.2 用 try-with-resources 替代手动 finally
// ❌ 传统写法:finally 容易出错
Connection conn = null;
try {
conn = getConnection();
conn.execute();
} finally {
if (conn != null) conn.close(); // 容易忘记 null 检查
}
// ✅ 正确写法:try-with-resources
try (Connection conn = getConnection()) {
conn.execute();
} // 自动 close,且异常不会互相覆盖