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块的作用

  1. 确保资源释放:

    • finally 块最主要的作用是确保无论 try 块中是否发生异常,也无论 catch 块是否捕获到异常,特定的代码段(通常用于资源清理和关闭资源)都会被执行。这对于需要手动管理资源的情况(如文件流、数据库连接、网络连接等)至关重要,避免资源泄漏。

    • finally 块常用于确保文件流、数据库连接、网络连接等资源的正确关闭。在 Java 中,这些资源若不及时关闭,可能导致资源泄漏,长时间运行后会耗尽系统资源,使程序性能下降甚至崩溃。

    • 我认为这是finally块在异常中被设计出来的初衷,因为我们也不知道也需要一个异常后被正确处理的情况。

    • 虽然现在,大家使用更多的是使用try-with-resources语法,因为它能够自动管理资源,减少错误发生的概率。省事还高级。


  1. 异常后执行清理工作:

    • finally块确保程序不会因为异常中断而漏掉必要的清理操作。这样可以避免资源泄漏或系统状态不一致的问题。

    • 其中,在涉及多层资源嵌套的场景中,finally 块的作用更为突出,多层资源之间的关系密切复杂,在finally块中去有条理的解决即友好又省事。因为finally 块确保了处理是成功还是因异常回滚,相关资源都能被正确释放。


  1. 对某些操作的保证: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);
    	}
    }
    

  1. 维护程序状态一致性

    • 确保部分操作完成:在某些业务逻辑中,部分操作完成后需要执行特定的收尾操作以维护程序状态的一致性。

    • 恢复中间状态:在一些复杂的业务流程中,程序可能会在执行过程中进入临时的中间状态。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);
    }
}

  1. 如果 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 块中的逻辑。可读性和可维护性会瞬间爆炸


  1. 异常情况下的返回:如果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,欢迎来玩!

posted @ 2024-12-30 21:19  ErgouTree  阅读(355)  评论(0)    收藏  举报
返回顶端