Java 的异常处理机制为程序员提供了一种有效的手段来处理运行时可能发生的各种错误和异常情况。它通过捕获和处理异常来避免程序的崩溃,同时还能进行适当的恢复或终止操作。下面将从多个角度对异常处理对程序执行流程的影响进行分类说明,并结合详细示例来说明其影响。
1. 正常执行流程与异常中断的流程
在没有异常的情况下,Java 程序按照顺序执行,每个语句依次运行。但在出现异常时,程序的正常执行流程会被中断,并且立即转到异常处理逻辑,跳过异常发生后到异常捕获之间的代码。具体来说,异常处理中断了当前的执行流程,将其交给适当的 catch
块或者上层调用者处理。
示例:正常流程与异常中断
public class ExceptionDemo {
public static void main(String[] args) {
System.out.println("Start of main method");
try {
int result = divide(10, 0); // 这里会抛出异常
System.out.println("This will not be printed");
} catch (ArithmeticException e) {
System.out.println("Caught an exception: " + e.getMessage());
}
System.out.println("End of main method");
}
public static int divide(int a, int b) {
return a / b; // 0 作为除数会抛出 ArithmeticException
}
}
执行流程分析:
- 当
divide(10, 0)
执行时,程序遇到除以零的情况,抛出了ArithmeticException
,程序的正常执行流程被中断。 - 异常被
catch
块捕获并处理,程序跳过了System.out.println("This will not be printed")
,而是直接执行catch
块中的代码。 - 最终,程序恢复执行,继续运行
catch
块之后的代码,即打印出 "End of main method"。
2. try-catch
结构中的控制流
在 try-catch
结构中,Java 程序执行到 try
块时,会尝试执行 try
块中的代码。如果没有异常发生,catch
块不会执行,程序继续执行 try-catch
之后的代码;如果 try
块中的代码抛出了异常,catch
块会被触发,控制权转移到 catch
块。
示例:try-catch
的控制流
public class TryCatchDemo {
public static void main(String[] args) {
try {
System.out.println("Inside try block");
int result = 10 / 0; // 抛出 ArithmeticException
System.out.println("This line will not be executed");
} catch (ArithmeticException e) {
System.out.println("Exception caught: " + e);
}
System.out.println("After try-catch block");
}
}
执行流程分析:
try
块中的int result = 10 / 0;
会抛出异常,导致try
块中断,跳转到catch
块。catch
块捕获异常后处理,并打印异常信息。- 最后,程序继续执行
catch
块之后的代码,即打印 "After try-catch block"。
3. finally
块的执行
finally
块是异常处理的一部分,无论是否抛出异常,finally
块中的代码都会被执行。它通常用于资源清理,如关闭文件、数据库连接等。
示例:finally
块的执行
public class FinallyDemo {
public static void main(String[] args) {
try {
System.out.println("Inside try block");
int result = 10 / 0; // 抛出 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Exception caught: " + e);
} finally {
System.out.println("Finally block executed");
}
}
}
执行流程分析:
try
块中发生ArithmeticException
,控制流转移到catch
块。- 无论是否有异常,
finally
块总会执行,打印 "Finally block executed"。
如果没有异常:
public class FinallyNoExceptionDemo {
public static void main(String[] args) {
try {
System.out.println("Inside try block");
int result = 10 / 2; // 没有异常
} catch (ArithmeticException e) {
System.out.println("Exception caught: " + e);
} finally {
System.out.println("Finally block executed");
}
}
}
执行流程:
- 没有异常,程序正常执行
try
块中的代码,跳过catch
,然后finally
块照常执行。
4. 抛出异常的执行流程(throw
和 throws
)
throw
:用于在程序中显式地抛出异常。抛出异常后,当前代码块的执行立即停止,程序跳转到异常处理器。throws
:用于方法签名,表示该方法可能会抛出指定类型的异常,调用者需要处理这些异常。
示例:显式抛出异常
public class ThrowDemo {
public static void main(String[] args) {
try {
validateAge(15); // 传入非法年龄
} catch (IllegalArgumentException e) {
System.out.println("Caught exception: " + e.getMessage());
}
}
public static void validateAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("Age must be 18 or older");
}
}
}
执行流程分析:
validateAge(15)
会显式抛出IllegalArgumentException
,异常会立即中断validateAge
的执行,转移到catch
块处理。
5. 多重异常捕获
Java 支持多个 catch
块处理不同类型的异常。当 try
块抛出异常时,JVM 会按照 catch
块的顺序依次检查,如果匹配到适当的异常类型,执行相应的 catch
块,其它 catch
块将被跳过。
示例:多重异常捕获
public class MultipleCatchDemo {
public static void main(String[] args) {
try {
int[] arr = new int[5];
arr[5] = 10; // 数组越界异常
int result = 10 / 0; // 除以零异常
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index out of bounds: " + e);
} catch (ArithmeticException e) {
System.out.println("Arithmetic exception: " + e);
}
}
}
执行流程分析:
- 数组越界时,程序进入
ArrayIndexOutOfBoundsException
的catch
块,打印异常信息,后面的ArithmeticException
不会被捕获。
6. 异常的传播(Exception Propagation)
如果一个方法中没有处理异常(即没有 try-catch
块),异常会沿着方法调用栈向上传递,直到找到适当的异常处理程序。如果所有调用链上都没有处理异常,程序会终止并打印堆栈信息。
示例:异常传播
public class ExceptionPropagationDemo {
public static void main(String[] args) {
try {
method1();
} catch (ArithmeticException e) {
System.out.println("Exception handled in main: " + e);
}
}
public static void method1() {
method2(); // 调用 method2
}
public static void method2() {
int result = 10 / 0; // 这里抛出 ArithmeticException
}
}
执行流程分析:
method2
抛出异常,但没有catch
块处理。- 异常传播到
method1
,但method1
也没有处理异常。 - 最终,异常被
main
方法中的catch
块捕获。
7. 受检异常与非受检异常的影响
Java 将异常分为两大类:受检异常(Checked Exception)和 非受检异常(Unchecked Exception)。
- 受检异常:必须在编译时处理,要么捕获要么声明抛出,例如
IOException
。 - 非受检异常:如
NullPointerException
、ArithmeticException
,这些是RuntimeException
的子类,不强制要求处理。
示例:受检异常的处理
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
public class CheckedExceptionDemo {
public static void main(String[] args) {
try {
File file = new File("nonexistent.txt");
FileReader fr = new FileReader(file); // 可能抛出 FileNotFoundException
} catch (FileNotFoundException e) {
System.out.println("File not found: " + e);
}
}
}
执行流程分析:
FileReader
可能抛出FileNotFoundException
,这是一种受检异常,必须在编译时处理。- 没有处理受检异常会导致编译错误。
8. .带资源的try,即try with resource, 可以自动关闭资源,当程序调出try 块之后,即执行到 catch 或 finally中的时候,资源已经关闭了,拿不到InputStream了
在 Java 中,try-with-resources
语法是为了简化资源的管理,并确保在使用完资源后自动进行清理(关闭)。通常,资源是指那些实现了 AutoCloseable
或 Closeable
接口的对象,例如文件、输入输出流、数据库连接等。try-with-resources
的核心作用就是在代码块执行完毕之后,无论是否发生异常,都会自动关闭资源。
try-with-resources
的执行流程
当使用 try-with-resources
语句时,资源会在 try
块执行结束或发生异常时自动关闭。这意味着当程序执行到 catch
或 finally
块时,资源已经关闭了,因此在 catch
或 finally
中无法再访问已关闭的资源(例如 InputStream
等)。
其执行流程如下:
- 资源初始化:在
try
块开始之前,所有资源会在try
语句中声明并初始化。 try
块执行:try
块中的代码正常执行,如果没有异常,则直接执行完后资源关闭。catch
块执行(可选):如果try
块中发生了异常,程序会跳转到相应的catch
块。- 资源自动关闭:不论是否抛出异常,资源都会在
try
块结束后自动关闭。 finally
块执行(可选):如果有finally
块,程序在关闭资源后会执行finally
中的代码。
try-with-resources
示例
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesDemo {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line = br.readLine(); // 正常读取文件
System.out.println(line);
} catch (IOException e) {
System.out.println("Caught an IOException: " + e.getMessage());
} finally {
System.out.println("Finally block executed");
}
}
}
执行流程:
- 资源初始化:
BufferedReader
和FileReader
在try
块的开头被创建。 try
块执行:br.readLine()
尝试读取文件的内容。如果读取成功且没有异常发生,try
块正常结束。- 资源关闭:无论是否抛出异常,在
try
块执行完毕或异常发生后,BufferedReader
和FileReader
都会被自动关闭。 catch
块执行(如果有异常):如果在try
块中发生了IOException
,程序跳到catch
块处理异常。finally
块执行:关闭资源之后,无论是否有异常,finally
块中的代码都会执行。
问题:catch
或 finally
中无法获取资源
由于 try-with-resources
语法确保资源在 try
块结束时自动关闭,因此在进入 catch
或 finally
块之前,资源已经被关闭了。如果尝试在 catch
或 finally
块中再次访问资源,例如 InputStream
,则会抛出异常,提示资源已关闭。
示例:无法在 catch
中访问已关闭的资源
import java.io.*;
public class TryWithResourcesCatchDemo {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("nonexistent.txt"))) {
String line = br.readLine(); // 此行会抛出异常
System.out.println(line);
} catch (IOException e) {
// 资源已经关闭,下面这行代码会抛出 IllegalStateException 或其他类似异常
try {
System.out.println("Trying to read after exception: " + br.readLine()); // 不能访问已关闭的资源
} catch (IOException ex) {
System.out.println("Caught another IOException: " + ex.getMessage());
}
}
}
}
在上述代码中,br.readLine()
在 catch
块中访问已关闭的 BufferedReader
,因此会抛出 IOException
,提示资源已关闭。
如何处理在 catch
或 finally
中访问资源的需求
如果必须在 catch
或 finally
块中访问资源的内容或获取未完全处理的输入/输出,可以采取以下策略:
-
提前读取并存储数据:如果在
try
块中需要读取输入流(如InputStream
或Reader
),可以在try
块内部先将数据读入一个变量或数据结构(如String
或byte[]
),然后在catch
或finally
中使用这些已保存的数据。 -
延迟关闭资源:手动管理资源的关闭,避免使用
try-with-resources
,以便在异常发生时可以在catch
或finally
中访问资源。但这种做法会让代码复杂化,需要手动确保资源在最终被正确关闭。
示例:提前读取数据
import java.io.*;
public class TryWithResourcesSolutionDemo {
public static void main(String[] args) {
StringBuilder fileContent = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) {
fileContent.append(line).append("\n");
}
} catch (IOException e) {
System.out.println("Exception caught: " + e.getMessage());
} finally {
System.out.println("File content: " + fileContent.toString());
}
}
}
在此示例中,数据在 try
块中被完整读取并存储在 fileContent
中,因此在 finally
块中能够访问文件的内容,而不需要直接依赖已关闭的 BufferedReader
对象。
总结
try-with-resources
语法在try
块结束后会自动关闭资源,无论是否发生异常。- 资源在
catch
和finally
中已经关闭,因此不能在这些块中再次访问资源,如InputStream
或Reader
。 - 为了解决在
catch
或finally
中访问资源的问题,可以提前将所需数据读取到内存中,或者选择手动管理资源的关闭(但这种做法不推荐,容易出错)。
9. .finally 块中抛出异常,会屏蔽掉 catch 块中异常
在 Java 中,finally
块中的异常确实会屏蔽掉 catch
块中抛出的异常。这是因为 finally
块的执行优先于 catch
块中抛出的异常的传播。在这种情况下,finally
块抛出的异常会取代 catch
块中的异常,导致程序无法感知和处理 catch
块抛出的异常。
让我们详细解释一下这个机制,并通过代码演示其效果。
异常处理流程
- 当一个异常在
try
块中被抛出时,程序会进入catch
块处理该异常。 - 不论
catch
块是否抛出异常,finally
块总是会执行。 - 如果
finally
块中抛出了新的异常,这个异常会覆盖之前的异常,并导致程序最终抛出的是finally
中的异常,而不是catch
中的异常。
示例:finally
块屏蔽 catch
块的异常
以下代码演示了 finally
块如何屏蔽 catch
块中的异常:
public class ExceptionDemo {
public static void main(String[] args) {
try {
throw new RuntimeException("Exception in try block");
} catch (RuntimeException e) {
System.out.println("Caught exception: " + e.getMessage());
throw new RuntimeException("Exception in catch block"); // 在 catch 中抛出新的异常
} finally {
System.out.println("Executing finally block");
throw new RuntimeException("Exception in finally block"); // 在 finally 中抛出新的异常
}
}
}
输出:
Caught exception: Exception in try block
Executing finally block
Exception in thread "main" java.lang.RuntimeException: Exception in finally block
执行流程分析:
try
块:首先,在try
块中抛出一个RuntimeException
,程序立即跳转到对应的catch
块。catch
块:在catch
块中,程序捕获到try
块中的异常,并打印"Caught exception: Exception in try block"
。然后,catch
块再次抛出一个新的异常(Exception in catch block
)。finally
块:在抛出catch
块中的异常之后,程序转向finally
块,finally
块的内容总是会执行。在finally
块中,再次抛出一个新的异常(Exception in finally block
)。- 异常屏蔽:尽管
catch
块中抛出了异常,但由于finally
块也抛出了异常,finally
块的异常会覆盖(屏蔽)掉catch
块的异常。因此,最终抛出的异常是finally
中的异常,而不是catch
中的异常。
为什么会发生这种情况?
在 Java 中,finally
块的设计目的是确保在不论发生什么情况时,都能执行清理工作或其他必要的后续操作。由于 finally
块优先于异常传播的执行顺序,一旦 finally
块抛出了异常,这个异常会直接影响程序的最终状态。Java 虚拟机会优先处理 finally
块的异常,而不是之前发生的异常。
如何处理这种情况?
为了避免 finally
块中的异常屏蔽掉 catch
块中的异常,常见的做法是:
- 不在
finally
块中抛出异常:确保finally
块中的代码尽量不抛出异常,或者对可能抛出的异常进行捕获和处理。 - 记录多个异常:如果必须抛出
finally
中的异常,可以考虑使用addSuppressed
方法将finally
中的异常附加到catch
块中的异常,确保不会丢失任何重要信息。
使用 addSuppressed
处理多个异常
Java 7 引入了 addSuppressed
方法,可以将多个异常一起记录下来。这样,catch
块中的异常不会被 finally
中的异常完全屏蔽。
以下是一个使用 addSuppressed
的示例:
public class ExceptionDemo {
public static void main(String[] args) {
RuntimeException primaryException = null;
try {
throw new RuntimeException("Exception in try block");
} catch (RuntimeException e) {
primaryException = e;
System.out.println("Caught exception: " + e.getMessage());
throw new RuntimeException("Exception in catch block");
} finally {
System.out.println("Executing finally block");
try {
throw new RuntimeException("Exception in finally block");
} catch (RuntimeException finallyException) {
if (primaryException != null) {
primaryException.addSuppressed(finallyException); // 将 finally 的异常附加到 catch 的异常上
} else {
throw finallyException; // 如果没有 catch 异常,直接抛出 finally 的异常
}
}
}
}
}
输出:
Caught exception: Exception in try block
Executing finally block
Exception in thread "main" java.lang.RuntimeException: Exception in catch block
Suppressed: java.lang.RuntimeException: Exception in finally block
在这个例子中:
try
块抛出了异常,导致catch
块处理异常。catch
块再次抛出了新的异常,但在finally
块中也抛出了异常。- 为了保留
catch
块中的异常,finally
块中的异常被附加为catch
异常的“抑制异常”(suppressed exception)。因此,程序不会丢失finally
块中的异常。
最终的输出显示了 catch
块的异常以及 finally
块中的抑制异常,解决了屏蔽问题。
总结
finally
块抛出的异常会屏蔽catch
块中的异常,这意味着最终抛出的异常会是finally
中的异常,而不是catch
中的异常。- 如果需要保留
catch
和finally
中的异常,可以通过 Java 7 引入的addSuppressed
方法将finally
块中的异常附加到catch
块的异常上,避免丢失信息。 - 避免在
finally
块中抛出异常,除非有明确的需求,通常finally
块应该专注于清理工作而非抛出新的异常。
10. **强烈不建议在finally中放置return 语句。
10.0.1.正常情况下 try块会缓存return 值,并在执行return 语句之前插入finally 块进行执行,故finally中的return 会执行,而方法就此return 了,try块中原本的return 语句不会被执行。
10.0.2.更重要的是try块中的return 会被缓存起来,然后执行 fiannly, 此时方法return基本类型 与 引用类型的情况是截然不同的,情况非常隐蔽,现在finally 块中很正常没有return,仅仅在finally 中修改了return 值, 其影响遵守值传递/引用传递原则,那么当方法return 为基本类型值的情况下,try 中被缓存的 return 值完全不会不会被finally块中的修改而影响;当方法return 为引用类型值的情况下,try 中被缓存的 return 值其实会被finally块中的修改而影响,遵循引用传递原则。而详情见如下示例分析
**
下面通过实验来看这几种情况的执行顺序到底是什么。
1、try中有return,finally中没有
public class TryCatchTest {
public static void main(String[] args) {
System.out.println("test()函数返回:" + test());
}
private static int test(){
int i = 0;
try {
System.out.println("Try block executing: " + ++i);
return i;
}catch (Exception e){
System.out.println("Catch Error executing: " + ++i);
return -1;
}finally {
System.out.println("finally executing: " + ++i);
}
}
}
结果如下:
Try block executing: 1
finally executing: 2
test()函数返回:1
return的是对象时,看看在finally中改变对象属性,会不会影响try中的return结果。
public class TryCatchTest {
public int vaule = 0;
public static void main(String[] args) {
System.out.println("test()函数返回:" + test().vaule);
}
private static TryCatchTest test(){
TryCatchTest t = new TryCatchTest();
try {
t.vaule = 1;
System.out.println("Try block executing: " + t.vaule);
return t;
}catch (Exception e){
t.vaule = -1;
System.out.println("Catch Error executing: " + t.vaule);
return t;
}finally {
t.vaule = 3;
System.out.println("finally executing: " + t.vaule);
}
}
}
Try block executing: 1
finally executing: 3
test()函数返回:3
2、try和finally中均有return
private static int test(){
int i = 0;
try {
System.out.println("Try block executing: " + ++i);
return i;
}catch (Exception e){
System.out.println("Catch Error executing: " + ++i);
return -1;
}finally {
System.out.println("finally executing: " + ++i);
return i;
}
}
结果如下:
Try block executing: 1
finally executing: 2
test()函数返回:2
3、catch和finally中均有return
private static int test(){
int i = 0;
try {
System.out.println("Try block executing: " + ++i);
throw new Exception();
}catch (Exception e){
System.out.println("Catch Error executing: " + ++i);
return -1;
}finally {
System.out.println("finally executing: " + ++i);
return i;
}
}
输出结果:
Try block executing: 1
Catch Error executing: 2
finally executing: 3
test()函数返回:3
总结
1、不管有没有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算之后执行的;
对于含有return语句的情况,这里我们可以简单地总结如下:
try语句在返回前,将其他所有的操作执行完,保留好要返回的值,而后转入执行finally中的语句,而后分为以下三种情况:
情况一:如果finally中有return语句,则会将try中的return语句“覆盖”掉,直接执行finally中的return语句,得到返回值,这样便无法得到try之前保留好的返回值。
情况二:如果finally中没有return语句,也没有改变要返回值,则执行完finally中的语句后,会接着执行try中的return语句,返回之前保留的值。
情况三:如果finally中没有return语句,但是改变了要返回的值,这里有点类似与引用传递和值传递的区别,分以下两种情况:
1)如果return的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块之前保留的值。
2)如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。