杰瑞科技汇

Java finally块会执行异常吗?

finally 块是什么?

finally 是 Java 异常处理机制中的一个关键字,它与 trycatch 块一起使用。finally 块的主要特点是:无论 try 块中是否发生异常,也无论异常是否被 catch 块捕获,finally 块中的代码都一定会被执行。

Java finally块会执行异常吗?-图1
(图片来源网络,侵删)

它的基本语法结构如下:

try {
    // 可能会抛出异常的代码
} catch (ExceptionType1 e1) {
    // 捕获并处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
    // 捕获并处理 ExceptionType2 类型的异常
} finally {
    // 无论是否发生异常,这里的代码都会被执行
    // 通常用于资源清理,如关闭文件、数据库连接等
}

finally 块的主要用途

finally 块最主要、最经典的用途就是资源清理

在很多情况下,程序会占用一些外部资源,

  • 文件(FileInputStream, FileOutputStream
  • 网络连接(Socket
  • 数据库连接(Connection, Statement

这些资源是有限的,如果不及时释放,会导致资源泄露,最终可能导致程序崩溃。

Java finally块会执行异常吗?-图2
(图片来源网络,侵删)

示例:不使用 finally 的风险

import java.io.FileInputStream;
import java.io.IOException;
public class WithoutFinally {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("test.txt");
            // 读取文件...
            int data = fis.read();
            if (data == -1) { // 假设读到文件末尾
                System.out.println("文件读取完毕");
                return; // 如果在这里 return,fis 就不会被关闭!
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // try 块中没有异常,并且没有执行 return,这里才会执行
        if (fis != null) {
            try {
                fis.close(); // 手动关闭,但很容易被遗忘或跳过
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

在上面的代码中,read() 方法返回 -1,程序会执行 return 语句,从而跳过后面的 fis.close(),导致文件句柄泄露。

使用 finally 解决问题

import java.io.FileInputStream;
import java.io.IOException;
public class WithFinally {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("test.txt");
            // 读取文件...
            int data = fis.read();
            if (data == -1) {
                System.out.println("文件读取完毕");
                return; // 即使在这里 return,finally 块也会执行
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 这是清理资源的最佳位置,无论何种情况都会执行
            if (fis != null) {
                try {
                    fis.close();
                    System.out.println("文件已成功关闭");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

使用 finally 后,无论 try 块是正常结束、因为异常中断,还是因为 return 语句退出,fis.close() 都会被保证执行,从而避免了资源泄露。


finally 块的执行规则(核心与难点)

finally 块的执行遵循一些非常重要的规则,理解这些规则对于写出健壮的代码至关重要。

规则 1:正常情况

  • try 块中的代码正常执行完毕,没有抛出任何异常。
  • 然后执行 catch 块(如果有的话)。
  • 执行 finally
public class NormalExecution {
    public static void main(String[] args) {
        try {
            System.out.println("在 try 块中");
            int a = 10 / 2;
            System.out.println("a = " + a);
        } catch (Exception e) {
            System.out.println("在 catch 块中");
        } finally {
            System.out.println("在 finally 块中");
        }
        System.out.println("程序结束");
    }
}
// 输出:
// 在 try 块中
// a = 5
// 在 finally 块中
// 程序结束

规则 2:try 块抛出异常并被 catch 捕获

  • try 块中的代码抛出异常。
  • 程序立即跳转到匹配的 catch 块执行。
  • catch 块执行完毕后,执行 finally
public class ExceptionCaught {
    public static void main(String[] args) {
        try {
            System.out.println("在 try 块中");
            int a = 10 / 0; // 抛出 ArithmeticException
            System.out.println("这行代码不会被执行");
        } catch (ArithmeticException e) {
            System.out.println("在 catch 块中,捕获到异常: " + e.getMessage());
        } finally {
            System.out.println("在 finally 块中");
        }
        System.out.println("程序结束");
    }
}
// 输出:
// 在 try 块中
// 在 catch 块中,捕获到异常: / by zero
// 在 finally 块中
// 程序结束

规则 3:try 块抛出异常但未被 catch 捕获

  • try 块中的代码抛出异常,但没有 catch 块能捕获它。
  • 程序会先执行 finally 块。
  • finally 块执行完毕后,异常会继续向外层(调用该方法的方法)传播。
public class ExceptionNotCaught {
    public static void main(String[] args) {
        try {
            System.out.println("在 try 块中");
            String s = null;
            System.out.println(s.length()); // 抛出 NullPointerException
        } catch (ArithmeticException e) { // catch 块不匹配
            System.out.println("在 catch 块中");
        } finally {
            System.out.println("在 finally 块中");
        }
        System.out.println("这行代码不会被执行");
    }
}
// 输出:
// 在 try 块中
// 在 finally 块中
// Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
//  at ExceptionNotCaught.main(ExceptionNotCaught.java:8)

注意:System.out.println("这行代码不会被执行"); 没有被打印,因为异常在 finally 执行后继续抛出,导致 main 方法中断。

规则 4:trycatch 块中有 return 语句

这是最关键也最容易出错的一点:finally 块的执行优先级高于 return 语句

  • trycatch 块执行到 return 语句时,它不会立即返回
  • 它会先去检查是否存在 finally 块。
  • 如果存在,它会先执行 finally 块中的所有代码。
  • 只有在 finally 块执行完毕后,才会执行 trycatch 块中的 return 语句,真正地返回值并退出方法。
public class FinallyWithReturn {
    public static int test() {
        int result = 10;
        try {
            System.out.println("try 块开始");
            return result; // 准备返回 10
        } finally {
            System.out.println("finally 块开始");
            result = 20; // 修改了 result 的值
            System.out.println("finally 块结束");
            // 注意:这里如果写 return result;,会覆盖 try 块中的 return
        }
    }
    public static void main(String[] args) {
        int value = test();
        System.out.println("方法返回的值是: " + value);
    }
}
// 输出:
// try 块开始
// finally 块开始
// finally 块结束
// 方法返回的值是: 10

解释try 块中的 return result; 在字节码层面,会先把返回值 10 压入一个“返回值栈”中,程序会去执行 finally 块。finally 块执行完毕后,方法会从“返回值栈”中取出之前压入的值 10,并将其返回。finally 块中对 result 的修改不会影响已经压入栈的返回值。

另一个例子:finally 块中的 return

finally 块中也包含了 return 语句,那么它会覆盖 trycatch 块中的 return

public class FinallyWithReturnOverride {
    public static int test() {
        try {
            System.out.println("try 块开始");
            return 10;
        } finally {
            System.out.println("finally 块开始");
            return 30; // 这个 return 会覆盖 try 块中的 return
        }
    }
    public static void main(String[] args) {
        int value = test();
        System.out.println("方法返回的值是: " + value);
    }
}
// 输出:
// try 块开始
// finally 块开始
// 方法返回的值是: 30

finally 块中使用 return 是一个非常糟糕的编程实践,因为它会隐藏原始的异常和返回值,使代码逻辑变得混乱且难以调试。

规则 5:trycatch 块中抛出异常

  • trycatch 块中抛出了一个异常(没有被内部捕获),程序会先去执行 finally 块。
  • finally 块中没有抛出新的异常,那么原始的异常会继续向外层传播。
  • finally 块中也抛出了一个异常,那么原始的异常会被丢弃finally 块中抛出的新异常会向外层传播。
public class FinallyWithException {
    public static void test() {
        try {
            System.out.println("try 块开始");
            throw new RuntimeException("来自 try 块的异常");
        } catch (Exception e) {
            System.out.println("catch 块开始");
            throw new RuntimeException("来自 catch 块的异常");
        } finally {
            System.out.println("finally 块开始");
            throw new RuntimeException("来自 finally 块的异常"); // 这会覆盖之前的所有异常
        }
    }
    public static void main(String[] args) {
        try {
            test();
        } catch (Exception e) {
            System.out.println("捕获到最终异常: " + e.getMessage());
        }
    }
}
// 输出:
// try 块开始
// catch 块开始
// finally 块开始
// 捕获到最终异常: 来自 finally 块的异常

finally 块中抛出异常同样是极其危险的,它会掩盖掉真正导致问题的原始异常。


Java 7+ 的 try-with-resources 语句

为了更优雅、更安全地处理资源,Java 7 引入了 try-with-resources 语句,它能够自动关闭实现了 AutoCloseable 接口(或 Closeable 接口)的资源,从而替代了手动 finally 块的写法。

语法

try (ResourceType resource = new ResourceType()) {
    // 使用资源
} // 资源在这里会自动关闭,无论是否发生异常

示例

import java.io.FileInputStream;
import java.io.IOException;
public class TryWithResources {
    public static void main(String[] args) {
        // try-with-resources 会自动调用 fis.close()
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            // 读取文件...
            int data = fis.read();
            System.out.println("读取到数据: " + data);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // fis 已经被自动关闭了,无需手动 finally
    }
}

优点

  1. 代码更简洁:消除了冗余的 finally 块。
  2. 更安全:资源一定会被关闭,即使发生异常或 return 语句。
  3. 可以同时管理多个资源:用分号隔开即可。
    try (FileInputStream fis = new FileInputStream("in.txt");
         FileOutputStream fos = new FileOutputStream("out.txt")) {
        // ...
    } catch (IOException e) {
        // ...
    }

特性/规则 描述 示例/建议
核心作用 保证代码一定会被执行,主要用于资源清理。 finally { resource.close(); }
执行顺序 优先级高于 return 和异常传播。 try { return; } finally { ... } 会先执行 finally 再返回。
return finally 中的 return 会覆盖 try/catch 中的 return 严禁finally 中使用 return
与异常 finally 中抛出的异常会覆盖 try/catch 中抛出的异常。 严禁finally 中抛出新的异常或吞没异常。
现代实践 优先使用 try-with-resources 来管理实现了 AutoCloseable 的资源。 try (MyResource r = ...) { ... }

一句话总结finally 是一个强大的工具,用于确保关键清理逻辑的执行,它的强大也伴随着风险,尤其是在与 return 和异常交互时,在现代 Java 开发中,对于资源管理,应优先考虑使用 try-with-resources

分享:
扫描分享到社交APP
上一篇
下一篇