首页 > 编程语言 >Java难绷知识04——异常处理中的finally块

Java难绷知识04——异常处理中的finally块

时间:2024-12-30 21:29:58浏览次数:7  
标签:Java 04 try finally catch return 异常 public

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

标签:Java,04,try,finally,catch,return,异常,public
From: https://www.cnblogs.com/ErgouTree/p/18642480

相关文章

  • Ubuntu 22.04 编译安装 PHP 7.4.33 报错:make: *** [Makefile:749: ext/openssl/openss
     下载openssl1.1.1  https://openssl-library.org/source/old/1.1.1/index.html安装低版本OpenSSLwgethttps://github.com/openssl/openssl/releases/download/OpenSSL_1_1_1w/openssl-1.1.1w.tar.gztarzxvfopenssl-1.1.1w.tar.gzcdopenssl-1.1.1w./config--prefi......
  • 基于Java的网络音乐系统的设计与实现
             摘 要互联网发展至今,无论是其理论还是技术都已经成熟,而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播,搭配信息管理工具可以很好地为人们提供服务。针对音乐信息管理混乱,出错率高,信息安全性差,劳动强度大,费时费力等问题,采用网络音乐系......
  • 【Java编程】JDBC 底层原理
    概述JDBC(JavaDataBaseConnectivity)是Java和数据库之间的一个桥梁,是一个规范而不是一个实现,能够执行SQL语句。JDBC由一组用Java语言编写的类和接口组成。各种不同类型的数据库都有相应的实现,注意:本文中的代码都是针对MySQL数据库实现的。先看一个案例:publicclassJdbcDemo{......
  • 解锁 Java 解释器模式:赋予程序理解 “新语言” 的魔力
    解锁Java解释器模式:赋予程序理解“新语言”的魔力在Java编程的广袤天地中,我们时常面临需要处理自定义规则、语法或逻辑表达式的场景。此时,解释器模式(InterpreterPattern)宛如一位神奇的翻译官,能够将这些看似晦涩难懂的“新语言”,转化为计算机能够理解并执行的指令,为......
  • 揭秘 Java 中介者模式:解耦复杂交互的神奇钥匙
    揭秘Java中介者模式:解耦复杂交互的神奇钥匙在Java开发的浩瀚天地里,随着系统复杂度的与日俱增,对象之间的交互常常变得错综复杂,宛如一团乱麻。此时,中介者模式(MediatorPattern)宛如一位智慧的协调大师,挺身而出,为我们理清理顺这些复杂的关系,打造更为优雅、易于维护的代码架......
  • 深入理解 Java 模板模式:代码复用与架构优化的利器
    深入理解Java模板模式:代码复用与架构优化的利器在Java编程世界中,设计模式如同智慧的结晶,帮助开发者应对各种复杂的软件开发需求。其中,模板模式(TemplatePattern)以其独特的代码复用和流程标准化能力,成为构建灵活且可维护系统的关键工具。今天,让我们一同深入探究Java中......
  • [2579]基于JAVA的粮油进销存智慧管理系统的设计与实现
    毕业设计(论文)开题报告表姓名学院专业班级题目基于JAVA的粮油进销存智慧管理系统的设计与实现指导老师(一)选题的背景和意义随着信息技术的快速发展,越来越多的企业开始采用信息管理系统来提升其业务运营效率。粮油行业作为我国国民经济的重要组成部分,其运营管理的高效性和......
  • [2570]基于JAVA的箱包进销存智慧管理系统的设计与实现
    毕业设计(论文)开题报告表姓名学院专业班级题目基于JAVA的箱包进销存智慧管理系统的设计与实现指导老师(一)选题的背景和意义一、选题背景随着信息技术的飞速发展,各行各业都开始尝试利用计算机技术和信息管理系统来提高工作效率和管理质量。箱包行业作为日常生活中的重要......
  • JAVA-Day 04:数据类型转换
    类型转换(Typeconversion)byte,short,char—>int—>long—>float—>doouble低---------------------------------------------------------------------->高注意:运算中,不同类型的数据先转化为同一类型,然后进行计算。类型转换(Typeconversion)分为强制转换和自动转换1.强制......
  • javascript promise同步化的三种方式。
    当async方法执行到await时,后面的代码就会整体被安排进一个新的微任务,此后的函数体变为异步执行。MDNasync:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await第一种:Promise队列串行letlist=[];lettaskQueue=[];//异步任务队......