Java难绷知识04——异常处理中的finally块
Java难绷知识04——异常处理中的finally块
前情提要:该文章是个人花的时间最长,查询资料最多,可能是有关finally块的最长文章,希望大家能看下去
一些前言
在Java中,异常处理机制是程序设计中至关重要的一部分。它允许程序员在程序运行时捕获并处理错误,防止程序因为异常情况而突然崩溃。
try - catch - finally结构是异常处理的核心部分。而finally块虽非必需,但为什么finally是异常处理中的最后一道防线
我的想法主要认为finally的必要关键之处是能够确保代码健壮性。
而且finally块中存在许多深入理解的地方,在这篇文章我将依旧侧重于finally在异常处理中的细节
try - catch - finally结构及其基础内容
try - catch - finally结构是Java异常处理的核心部分。它允许你在代码出现错误时进行适当的处理,而不是让程序崩溃。
在这里只对其简单阐述,本篇文章侧重点是finally
基本结构
try { // 可能抛出异常的代码 } catch (ExceptionType e) { // 异常处理代码 } finally { // 无论是否发生异常,都会执行的代码 }
try语句
作用:try语句块用于包含可能抛出异常的代码
它是异常监控的起始点,我们需要将可能出现问题的代码段放在 try 块内。
其中:
一个 try 块后必须至少跟一个 catch 块或者一个 finally 块。不能单独存在 try 块。
try 块内的代码一旦抛出异常,异常抛出点之后的代码将不会继续执行,程序流程会立即跳转到相应的 catch 块
catch语句:
作用:catch 块用于捕获并处理 try 块中抛出的异常。每个 catch 块指定了它能够捕获的异常类型。
在 catch 块内,你可以对捕获到的异常进行处理,例如记录日志、向用户显示更友好的错误信息、进行恢复操作等。捕获到的异常对象可以通过 catch 块的参数(如 e)来访问,通过这个对象可以获取异常的详细信息
其中:
catch 块会按顺序检查,只有与抛出异常类型匹配(包括子类类型匹配)的 catch 块才会被执行。
示例代码如下:
try { // 有一些语句抛出了 IOException } catch (IOException e)) { // 那么 catch (IOException e) 块会先被执行 // 如果没有 catch (IOException e),才会执行 catch (Exception e) } catch (Exception e) { }
多个 catch 块顺序:在编写多个 catch 块时,子类异常的 catch 块必须放在父类异常的 catch 块之前。否则,编译器会报错,因为子类异常永远无法被捕获。例如,以下代码会报错:
try { // 有一些语句抛出了 IOException } catch (Exception e) { } catch (IOException e) { // 编译器会报错,因为子类异常永远无法被捕获 }
catch语句可以有多个
finally语句
finally语句块是可选的
无论try块是否抛出异常,finally块代码通常都会执行。
它允许程序员在程序运行时捕获并处理错误,防止程序因为异常情况而突然崩溃。
如下代码片段验证finally的执行情况
try { System.out.println("Inside try block"); } catch (Exception e) { System.out.println("Exception caught"); } finally { System.out.println("Finally block always executes"); }
它主要用于放置必须执行的清理代码,如关闭文件流、释放数据库连接等。
finally块中的代码总是在try和catch执行之后、方法返回之前执行。即使在try或catch中出现了return语句,finally块依然会执行。
finally的基础知识
finally块的作用
-
确保资源释放:
-
finally 块最主要的作用是确保无论 try 块中是否发生异常,也无论 catch 块是否捕获到异常,特定的代码段(通常用于资源清理和关闭资源)都会被执行。这对于需要手动管理资源的情况(如文件流、数据库连接、网络连接等)至关重要,避免资源泄漏。
-
finally 块常用于确保文件流、数据库连接、网络连接等资源的正确关闭。在 Java 中,这些资源若不及时关闭,可能导致资源泄漏,长时间运行后会耗尽系统资源,使程序性能下降甚至崩溃。
-
我认为这是finally块在异常中被设计出来的初衷,因为我们也不知道也需要一个异常后被正确处理的情况。
-
虽然现在,大家使用更多的是使用try-with-resources语法,因为它能够自动管理资源,减少错误发生的概率。省事还高级。
-
-
异常后执行清理工作:
-
finally块确保程序不会因为异常中断而漏掉必要的清理操作。这样可以避免资源泄漏或系统状态不一致的问题。
-
其中,在涉及多层资源嵌套的场景中,finally 块的作用更为突出,多层资源之间的关系密切复杂,在finally块中去有条理的解决即友好又省事。因为finally 块确保了处理是成功还是因异常回滚,相关资源都能被正确释放。
-
-
对某些操作的保证:finally 块会影响 return 语句的执行流程,确保在返回值确定前执行必要的清理操作。即使try或catch语句中发生了return语句,finally块的代码依然会执行,保证了关键代码的执行。我们可以利用这个来处理异常发生后的操作。
public class FinallyWithReturnExample { public static int test() { try { // 当 try 块执行到 return 1 时,会先暂存返回值 1,然后执行 finally 块中的代码,最后再返回暂存的 1 return 1; } finally { // finally 块在 return 语句真正返回前执行,在有 return 的情况下,也能保证清理等必要收尾操作的执行,前提是你finally块中没有retrun语句 System.out.println("Finally block in test method"); } } public static void main(String[] args) { int result = test(); System.out.println("Result: " + result); } }
-
维护程序状态一致性
-
确保部分操作完成:在某些业务逻辑中,部分操作完成后需要执行特定的收尾操作以维护程序状态的一致性。
-
恢复中间状态:在一些复杂的业务流程中,程序可能会在执行过程中进入临时的中间状态。finally 块可用于在异常发生时恢复到之前的稳定状态。
public class OrderProcessingExample { private static String orderStatus = "INITIAL"; public static void processOrder() { // try 块尝试处理订单并更新订单状态 try { orderStatus = "PROCESSING"; // 模拟订单处理的复杂逻辑,可能抛出异常 if (Math.random() > 0.5) { throw new RuntimeException("Order processing failed"); } orderStatus = "COMPLETED"; } catch (Exception e) { e.printStackTrace(); // finally 块会检查订单状态 } finally { // 如果不是 COMPLETED,则将其恢复到 INITIAL 状态 // 保证程序状态的一致性和准确性。 if (!"COMPLETED".equals(orderStatus)) { orderStatus = "INITIAL"; } System.out.println("Final order status: " + orderStatus); } } public static void main(String[] args) { processOrder(); } } - 增强代码的健壮性与可维护性:finally 块为异常处理提供了一个统一的出口,无论 try 块中发生何种异常,都能在此进行统一的处理逻辑。这使得代码结构更加清晰,易于理解和维护。而且这样能够大量的减少代码重复。
-
所以这就是为什么要有异常捕获结构中要有finally块。
finally关键字的细节之处
有异常但未被捕获时,finally块的执行情况
finally块的执行与异常是否被捕获和处理是相对独立的。即使异常未被捕获,finally块也会执行其代码。这确保了无论异常如何传播,finally块中的资源清理或其他关键代码都能得到执行。
这里也可以看出finally块的必定会被执行的一个性质
finally块执行完毕后,向外传播的异常类型和try块中抛出的异常类型一致,不会因为finally块的存在而改变。但是,如果finally块中的代码抛出了异常,它会覆盖try块或catch块中已经抛出的异常
例如,如果try块抛出IOException,即使经过finally块的执行,向外传播的依然是IOException。
示例代码如下
public class FinallyThrowsExceptionExample { public static void main(String[] args) { try { methodThatThrowsException(); } catch (Exception e) { System.out.println("Caught in main: " + e.getMessage()); } } // main方法捕获到的异常信息是Caught in main: Exception thrown in finally public static void methodThatThrowsException() { try { // 原始try块中的异常被覆盖 throw new RuntimeException("Exception thrown in inner try"); } finally { throw new RuntimeException("Exception thrown in finally"); } } }
与 return 语句的交互
首先,在 finally 代码块中改变返回值并不会改变最后返回的内容,而且finally中的语句一定会执行
1.当 try 代码块和 catch 代码块中有 return 语句时,finally 仍然会被执行。且 try 代码块或 catch 代码块中的 return 语句执行之前,都会先执行 finally 语句
public class TryReturnFinallyExample { public static int test() { try { int result = 10 / 0; return result; } catch (ArithmeticException e) { return 2; } finally { System.out.println("Finally block in test method"); } } public static void main(String[] args) { int result = test(); System.out.println("Result: " + result); } }
2.finally 块中的代码可以访问和修改 try 块和 catch 块中定义的局部变量,但这种修改不会影响 return 语句返回的值
public class CatchReturnFinallyVariableExample { public static int test() { try { int result = 10 / 0; return result; } catch (ArithmeticException e) { int num = 2; return num; } finally { // finally 块将 num 修改为 3 num = 3; } } // 但 return 语句返回的还是 catch 块中 return 语句执行时 num 的值 public static void main(String[] args) { int result = test(); System.out.println("Result: " + result); } }
如果此时,finally 块本身也有 return 语句,会以一种较为复杂的方式处理局部变量
try和catch块中的局部变量:即便finally块可以访问并修改try和catch块中定义的局部变量,由于finally块中的return会主导返回值,所以这种修改对最终返回值的影响也会被finally块的return逻辑所掩盖。
当在try块暂存return的结果时候,如果finally块修改了局部变量影响了返回值,但本质是finally块的return起了决定性作用。
示例代码
public class FinallyModifyLocalVar { public static int test() { int num = 1; try { return num; } finally { num = 3; return num; } } public static void main(String[] args) { int result = test(); System.out.println("Result: " + result); } }
- 如果 finally 块中有 return 语句,它会覆盖 try 或 catch 块中的 return 语句。这意味着无论 try 或 catch 块中原本打算返回什么值,最终都会被 finally 块中的 return 值取代。
public class FinallyReturnOverrideExample { public static int test() { try { // 尽管 try 块原本要返回 1 return 1; } catch (Exception e) { return 2; } finally { // 但由于 finally 块中有 return 3,最终返回的值是 3。 return 3; } } public static void main(String[] args) { int result = test(); System.out.println("Result: " + result); } }
所以说finally块中最好不要包含 return 语句,要不然程序会提前退出,,而且使用 finally 块中的 return 语句会使代码的逻辑变得混乱,因为它打破了正常的 try - catch - finally 异常处理流程,使得代码的返回值不依赖于 try 或 catch 块中的逻辑。可读性和可维护性会瞬间爆炸
- 异常情况下的返回:如果try块抛出异常,catch块捕获并处理异常,finally块的return语句依然会生效,覆盖catch块中的return
public class FinallyReturnWithException { public static int test() { try { int result = 10 / 0; return result; } catch (ArithmeticException e) { return 2; } finally { return 3; } } public static void main(String[] args) { int result = test(); System.out.println("Result: " + result); } }
在这里强调一下,如果出现了异常未捕获的情况,就是try块抛出异常且未被catch块捕获,那么finally块执行完毕后,finally块中的return会阻止异常继续传播,并且返回finally块中的值。(这种情况可能会隐藏程序中的异常,导致调试难度从Galgme变成黑暗之魂,别用)
异常屏蔽
首先要知道一个前提:
try 块抛出异常且 catch 块未捕获:当 try 块抛出异常,而 catch 块没有捕获该异常时,finally 块依然会执行。执行完 finally 块后,异常会继续向外层传递。
public class ExceptionFinallyInteractionExample { public static void test() { try { throw new RuntimeException("Exception in try block"); } finally { System.out.println("Finally block in test method"); } } public static void main(String[] args) { try { test(); } catch (RuntimeException e) { System.out.println("Caught in main method: " + e.getMessage()); } } }
如果finally块中的代码抛出了异常,它会覆盖try块或catch块中已经抛出的异常。
所以我们应该尽量避免在finally块中抛出异常,因为会覆盖异常本身的情况,导致调试出现歧义
public class FinallyThrowsExceptionExample { public static void test() { try { throw new RuntimeException("Exception in try block"); } finally { throw new RuntimeException("Exception in finally block"); } } // main 方法捕获到的是 finally 块抛出的异常信息 Exception in finally block public static void main(String[] args) { try { test(); } catch (RuntimeException e) { System.out.println("Caught in main method: " + e.getMessage()); } } }
finally中可能抛出异常的情况的处理
< 引用自 https://blog.csdn.net/qq_44861675/article/details/106353369 本人作补充
有这样一段代码
package Stream_IntOut; import java.io.*; /** * 使用缓冲区输入流和缓冲区输出流实现复制文件的功能。 * 并简单处理IO异常 * */ public class Practice3_BufferedWriter_BufferedReader_Copy { public static void main(String[]args){ FileWriter fw = null; FileReader fr = null; BufferedWriter bufw = null; BufferedReader bufr = null; try{ fw = new FileWriter("E:\\file_copy2.txt"); fr = new FileReader("E:\\file.txt"); bufw = new BufferedWriter(fw); bufr = new BufferedReader(fr); String line; while((line=bufr.readLine())!=null){ bufw.write(line); //写入换行符 bufw.newLine(); //刷新一次流对象 bufw.flush(); } }catch(IOException e){ e.printStackTrace(); }finally { if(fr!=null) try{ assert bufr != null; bufr.close(); }catch (IOException e){ throw new RuntimeException("无法关闭fr流对象"); } if(fw!=null) try{ assert bufw != null; bufw.close(); }catch (IOException e){ throw new RuntimeException("无法关闭fw流对象"); } } } }
我们可以从IDEA的提示里边看到一些东西: throw inside “finally” block
也就是说,finally块里边抛出异常是不建议的,java异常语句中的finally块通常用来做资源释放操作,finally块和普通代码块一样,无法同时使用return语句和throw语句,因为无法通过编译
为什么不被建议?
finally块中的throw语句会覆盖try和catch语句中的异常
实例代码
package 面试题; public class FinallyAndReturnAndThrow3 { public static void main(String[]args){ displayTest(); } private static void displayTest() { try{ System.out.println(2/0);//异常发生 }catch (Exception e){ System.out.println("displayTest's catch"); throw new RuntimeException("除数为0"); }finally { System.out.println("displayTest's finally"); throw new RuntimeException("俺会覆盖catch的异常"); } } }
在结果中,返回的异常是finally里面的,catch的异常并没有被抛出。同样的try中捕抓的异常也会被掩盖。
在Java核心技术书中,作者建议在finally块中尽量不要使用会抛出异常的资源回收语句。
那么在我们使用IO流时,常常在finally使用到throw,那该如何解决呢?
其中一个方法,就是接下来说的,在finally
块中使用try-catch
块,进行多层嵌套的try - catch - finally情况
但其实,大家更常用的方法就是 使用 Java 7 的try-with-resources
语句,在关闭资源时抛出的异常会被添加为原来异常的被抑制异常并展示,不会掩盖try
块中的异常。
多层嵌套的try - catch - finally情况
异常捕获顺序
- 内层优先:当异常发生时,Java 首先会尝试在最内层的
try
块对应的catch
块中捕获异常。如果内层try
块没有匹配的catch
块,异常会向外层try
块传播,寻找匹配的catch
块。 - 也需要遵循子类优先:在编写
catch
块时,捕获子类异常的catch
块应该放在其父类异常的catch
块之前。否则,子类异常的catch
块永远不会被执行,编译器会报错。
finally块的执行顺序
- 内层优先:无论异常是否发生,内层
try
块的finally
块总是在内层try
块结束时(正常结束或因异常结束)立即执行,然后才会执行外层try
块的finally
块。 - 异常传递:如果内层
try
块的finally
块抛出异常,这个异常会向外层传播,可能会掩盖内层try
块中原本抛出的异常。为避免这种情况,可以在内层finally
块中捕获并处理异常,或者使用辅助变量记录内层try
块的异常,同时处理内层finally
块抛出的异常。
注意资源的关闭顺序,永远是在多层嵌套中需要注意的地方
所以我建议使用try - with - resources
语句,它会自动管理资源的关闭,并确保每个资源只被关闭一次。
示例代码
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class NestedTryCatchExample { public static void main(String[] args) { try { try { InputStream inputStream = new FileInputStream("example.txt"); try { int data; while ((data = inputStream.read())!= -1) { System.out.print((char) data); } } catch (IOException e) { System.out.println("读取文件时出错: " + e.getMessage()); } finally { try { if (inputStream!= null) { inputStream.close(); } } catch (IOException e) { System.out.println("关闭文件时出错: " + e.getMessage()); } } } catch (FileNotFoundException e) { System.out.println("文件未找到: " + e.getMessage()); } } catch (Exception e) { System.out.println("发生其他异常: " + e.getMessage()); } } }
上一篇:Java难绷知识03--包装器类及其自动装箱和拆箱
下一篇:Java难绷知识05——Swing中的事件调度线程和资源释放
文章个人编辑肯定会有各种欠缺和漏洞,需要大家积极反馈来帮助这篇文章和我的技术知识的更进一步,也有不合理的地方需要大家指出,感谢每一位读者
QQ:1746928194,是喜欢画画的coder,欢迎来玩!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具