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,欢迎来玩!