「Java 进阶之路」系列 Day28
写在前面
try-catch-finally看起来是最基础的语法,但只要return和finally混在一起用,行为就开始变得反直觉。这篇把几个具体场景过一遍,讲清楚finally到底是在什么时机、以什么方式介入了方法的返回过程。
一、是什么:finally 几乎总会执行
finally块的设计初衷是"无论try块是正常结束、提前return、还是抛出了异常,都要执行的收尾逻辑",典型场景是关闭文件流、释放锁这类必须执行的清理动作。几乎总会执行这个说法留了个口子——只有System.exit()直接终止JVM进程、或者JVM自身崩溃这种极端情况下,finally才会被跳过,正常的业务代码逻辑里可以认为它是"必定执行"的。
二、为什么会有坑:return 的真正时机
public static int test() {
try {
return 1;
} finally {
return 2;
}
}
System.out.println(test()); // 输出2,不是1!
flowchart TB
A[try块执行到return 1这行] --> B[计算好返回值1 先暂存起来 还没真正返回给调用方]
B --> C[开始执行finally块]
C --> D[finally块里也有return语句]
D --> E[用finally的返回值2 直接覆盖掉之前暂存的1]
E --> F[方法最终返回2]
关键在于:try块里的return语句,并不是"立刻把值交出去",而是先把返回值计算好、暂存起来,然后再去执行finally块。如果finally块里也有一条return语句,这条return会直接把之前暂存的值覆盖掉,最终返回的是finally里的值。这也是为什么几乎所有代码规范都建议不要在finally里写return——它的优先级比try/catch里的return更高,很容易在不经意间覆盖掉本该返回的结果,而且极难在代码审查时一眼看出问题。
一个更隐蔽的例子:finally 修改变量,不一定能改变返回值
public static int test2() {
int i = 1;
try {
return i;
} finally {
i = 2; // 修改变量i,但不写return
}
}
System.out.println(test2()); // 输出1,不是2!
这里没有在finally里写return,但很多人会误以为"finally改了i,返回值也应该跟着变成2"。实际上执行到try里的return i时,i当前的值(1)已经被复制了一份暂存起来,作为这个方法准备返回的结果;随后执行finally块,i = 2这行改的是变量i本身,并不会去追溯、修改那份已经复制出去、暂存好的返回值。只有finally里再显式写一条return语句,才会真正覆盖返回结果;单纯在finally里修改一个基本类型变量的值,不会影响已经暂存的返回值。
(如果try里return的是一个引用类型对象、finally里修改的是这个对象内部的字段而不是重新赋值这个引用,情况又不一样——因为暂存的是这个对象的引用本身,finally对它内部字段的修改,调用方拿到返回值后是能感知到的。这里的关键始终是"暂存的到底是什么":基本类型暂存的是值的拷贝,引用类型暂存的是引用本身。)
最容易被打问号的坑:finally 里的 return 会把异常也吞掉
public static int test3() {
try {
throw new RuntimeException("出错了");
} finally {
return 2; // 这个return会把上面的异常直接吞掉!
}
}
System.out.println(test3()); // 输出2,异常完全消失,调用方感知不到出过错
这是最危险的一种情况:try块里抛出的异常,本来应该继续往外传播,但如果finally块里恰好也有一条return语句,这条return会直接终止异常的传播过程,异常被无声无息地吞掉,调用方完全不知道方法内部曾经抛出过异常,只会拿到finally返回的这个值。这也是为什么"不要在finally里写return"这条建议要格外强调——它不仅会覆盖try/catch里准备好的正常返回值,还会把原本应该继续抛出的异常直接吃掉,属于比"覆盖返回值"更隐蔽、更危险的一类问题。
三、怎么用:一条经验总结
综合以上几种情况,实际写代码时只需要记住一条经验:finally块只用来做资源清理、释放锁这类收尾动作,永远不要在finally里写return或者throw语句。只要遵守这一条,try-catch-finally组合起来的行为就会符合直觉——try正常执行完或者提前return,走try/catch那部分的返回值和异常;finally执行完清理逻辑后,安静地把控制权交还,不做任何"抢戏"的动作。
四、面试追问
Q1:try 里有 return,finally 里也有 return,最终返回的是哪个?
返回的是finally里的值。try块的return语句会先计算好返回值并暂存,但不会立刻返回,而是先执行finally块;如果finally块里也有return语句,会直接用这个新的返回值覆盖掉之前暂存的值,所以最终返回的是finally里return的结果。
Q2:try 里 return 了一个基本类型变量,finally 里修改了这个变量的值,返回值会变吗?
不会变,前提是finally里没有再写return语句。try块执行return时,变量当前的值已经被复制了一份暂存起来作为准备返回的结果,之后finally块对变量本身的修改,不会影响这份已经暂存的拷贝。只有finally里显式再写一条return语句,才会真正覆盖最终的返回结果。
Q3:如果 try 里的返回值是一个对象引用,finally 里修改了这个对象内部的字段,调用方能感知到吗?
能感知到。因为try块return时暂存的是这个对象的引用本身,不是对象内容的深拷贝;finally里如果修改的是这个引用指向的对象内部的字段(而不是把引用重新指向另一个新对象),调用方拿到的返回值和finally修改的是同一个对象,自然能看到这些字段变化。这一点和"基本类型变量的值不会受finally影响"是不同的机制,区分标准在于暂存的到底是值的拷贝还是对象的引用。
Q4:为什么说 finally 里的 return 会把 try 里抛出的异常吞掉?
因为finally块的return语句会直接终止整个方法的执行流程、把控制权作为正常返回交还给调用方,这个过程会覆盖并终止掉本应该继续向外传播的异常。如果try块里抛出了异常,而finally块恰好也有一条return语句被执行到,这个异常就会被无声无息地吞掉,调用方完全感知不到方法内部曾经出过错,只会拿到finally返回的值,这是一种非常隐蔽、容易被忽视的问题。
Q5:写 try-catch-finally 时应该遵循什么原则,避免这些坑?
finally块应该只用来做资源清理、释放锁这类收尾动作,不应该在finally里写return语句,也不应该在finally里抛出新的异常(同样会覆盖掉try/catch里原本要抛出的异常)。只要遵守这条原则,try/catch里准备好的返回值和异常就不会被finally意外覆盖或者吞掉,代码行为也更符合直觉、更容易被正确理解。
下一篇预告
Day29 讲字节流和字符流的区别——为什么处理文本文件更推荐用字符流,BufferedInputStream这类带缓冲的实现又是靠什么变快的。