27-Java|异常处理
异常的概念
什么是异常?
- 指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。
异常的抛出机制
- Java 中把不同的异常用不同的类表示,一旦发生某种异常,就
创建该异常类型的对象,并且抛出(throw)。然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获(catch)这个异常对象,那么这个异常对象将会导致程序终止。
如何对待异常
- 对于程序出现的异常,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是程序员在编写程序时,就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,要编写相应的代码进行异常的检测、以及
异常的处理,保证代码的健壮性。
异常的体系

在 Java 中,异常是通过类来表示的,所有异常类都是java.lang.Throwable类的子类。Throwable类有两个直接子类:
Error: 表示系统级的错误,通常是由 JVM 抛出的,程序一般不应捕获此类异常。例如:OutOfMemoryError。Exception: 表示程序中可以捕获和处理的异常。
Exception类又分为两类:
- Checked Exception(受检异常): 必须在编译时处理的异常。程序在编译时如果没有对这些异常进行处理(通过try-catch或者throws声明),会导致编译错误。例如:
IOException,SQLException。 - Unchecked Exception(非受检异常): 运行时异常,不强制要求在编译时处理,通常是由程序逻辑错误引起的,例如:
NullPointerException,ArrayIndexOutOfBoundsException。
运行时异常
在 Java 中,运行时异常(RuntimeException)通常是程序逻辑错误引起的,例如除零错误、空指针引用、数组越界等。由于运行时异常是非受检异常,Java编译器不会强制要求你处理这些异常,但你仍然可以使用try-catch块来捕获并处理它们,以防止程序崩溃。
示例 1
NullPointerException
1 | public class RuntimeExceptionExample { |
输出结果:
1 | 发生了空指针异常: Cannot invoke "String.length()" because "str" is null |
代码分析:
String str = null;:我们定义了一个String类型的变量str,并将其初始化为null。try块:我们在try块中尝试调用str.length()方法,因为str是null,所以会抛出NullPointerException。catch块:当NullPointerException发生时,程序跳转到catch块,捕获这个异常,并打印错误信息。这里,e.getMessage()返回异常的详细信息。finally块:finally块中的代码无论是否发生异常都会执行,用来清理资源或执行一些必要的操作。- 程序继续运行:尽管发生了异常,由于我们在
catch块中处理了它,程序能够继续运行,不会崩溃。
示例 2
ArrayIndexOutOfBoundsException
1 | public class ArrayExceptionExample { |
输出结果:
1 | 数组索引越界: Index 5 out of bounds for length 3 |
代码分析:
int[] numbers = {1, 2, 3};:我们定义了一个包含3个整数的数组。try:尝试访问数组的第6个元素(索引5),这将抛出ArrayIndexOutOfBoundsException,因为数组中只有3个元素。catch块:捕获并处理ArrayIndexOutOfBoundsException,打印异常信息。finally块:最后,无论是否发生异常,finally块都会执行,表示数组处理操作结束。- 程序继续运行:捕获并处理异常后,程序继续正常运行。
总结:
通过捕获和处理运行时异常,程序可以避免在发生错误时直接崩溃,从而提高程序的健壮性和用户体验。虽然Java不强制要求处理运行时异常,但在关键部分处理这些异常是编写健壮代码的好习惯。
编译时异常
Java 中的编译异常(Checked Exceptions)是必须在编译时处理的异常。它们通常用于表示程序在正常执行流程中可能出现的可预见性错误(如文件未找到、数据库连接失败等)。如果程序没有对这些异常进行处理(通过try-catch块或throws声明),编译器将会报错,无法通过编译。
处理机制
编译异常的处理有两种主要方式:
try-catch块:在方法内部捕获并处理异常。throws声明:在方法签名中声明抛出异常,将异常抛给调用者处理。
示例 1
try-catch块可以用来捕获和处理编译异常,确保程序在发生错误时能够进行适当的处理。
处理IOException(例如文件未找到异常)的示例:
1 | import java.io.BufferedReader; |
输出结果(假设文件不存在):
1 | 发生IO异常: example.txt (No such file or directory) |
代码分析:
try块:尝试打开一个名为example.txt的文件并读取其内容。catch块:如果文件不存在或无法读取,将抛出IOException,catch块捕获并处理该异常。finally块:确保文件读取器在操作完成后被正确关闭,避免资源泄漏。
示例 2
另一种处理编译异常的方式是在方法签名中使用throws关键字,将异常抛给方法的调用者处理。
抛出IOException
1 | import java.io.BufferedReader; |
输出结果(假设文件不存在):
1 | 发生IO异常: example.txt (No such file or directory) |
代码分析:
throws声明:readFile方法在其签名中使用throws IOException,表示该方法可能会抛出IOException,调用者需要处理此异常。- 调用者处理异常:在
main方法中,调用readFile方法时使用try-catch块捕获并处理IOException。
总结
编译异常的处理机制确保了程序在可能出现的错误情况下能够进行适当的处理,从而提高程序的健壮性。通过使用try-catch块处理异常,程序可以直接处理错误;通过throws声明,异常可以被传递给调用者,使得调用者可以根据上下文进行处理。这种机制使得Java的异常处理更加灵活和强大。
自定义异常
在 Java 中,自定义异常是开发人员根据程序的需求创建的异常类,用于处理特定的业务逻辑错误或异常情况。虽然Java已经提供了大量的内置异常类,但在某些情况下,这些内置异常可能无法准确描述特定的问题或业务场景。自定义异常可以帮助我们更清晰地表达这些特殊情况,从而提高代码的可读性和可维护性。
自定义异常的步骤
- 继承
Exception类或其子类:- 如果希望创建一个受检异常(必须处理的异常),需要继承
Exception类。 - 如果希望创建一个非受检异常(不强制处理的异常),可以继承
RuntimeException类。
- 如果希望创建一个受检异常(必须处理的异常),需要继承
- 提供构造方法:
- 通常会提供一个默认的无参构造方法,以及一个可以接受异常信息的构造方法,方便在抛出异常时传递详细信息。
- 抛出自定义异常:
- 在程序中适当的位置使用
throw关键字抛出自定义异常。
- 在程序中适当的位置使用
自定义受检异常
以下示例展示了如何创建一个自定义的受检异常InvalidAgeException,用于处理用户年龄不合法的情况。
1 | // 自定义异常类,继承Exception |
输出结果:
1 | 捕获到异常: 年龄必须在18到100岁之间。 |
自定义非受检异常
以下示例展示了如何创建一个自定义的非受检异常InsufficientFundsException,用于处理银行账户余额不足的情况。
1 | // 自定义异常类,继承RuntimeException |
输出结果:
1 | 捕获到异常: 您的账户余额不足,无法提取: 1500.0 |
代码分析
InvalidAgeException和InsufficientFundsException**是两个自定义异常类,分别继承了Exception和RuntimeException。- 这两个类都提供了默认构造方法和接受自定义错误信息的构造方法,使得在抛出异常时可以提供详细的错误描述。
- 在
BankAccount类和CustomExceptionExample类中,通过检查条件,如果不满足要求,抛出自定义异常,并在main方法中通过try-catch块捕获这些异常,进行相应的处理。
自定义异常的好处
- 表达清晰:自定义异常可以更准确地表达特定的业务错误,使代码更加清晰易懂。
- 增强可维护性:通过自定义异常,可以将特定错误情景与普通逻辑代码分离,提高代码的可维护性。
- 统一异常处理:在大规模应用中,可以统一处理某类特定的业务异常,从而提高异常处理的一致性和代码质量。
通过合理使用自定义异常,可以使 Java 应用程序的异常处理机制更加灵活和高效。
其他
在 Java 中,当使用try-catch-finally结构时,即使catch块中有return语句,finally块中的代码仍然会执行,而且是在catch块中的return语句执行之前。也就是说,finally块的执行优先级高于catch块中的return。
示例代码
1 | public class TryCatchFinallyExample { |
输出结果:
1 | 捕获异常: / by zero |
代码分析:
try块:尝试执行10 / 0,会抛出ArithmeticException,所以直接跳转到catch块,result的值不会被返回。catch块:捕获到ArithmeticException,打印“捕获异常: / by zero”,并且遇到return 1;准备返回值1。但在真正返回值之前,Java会先执行finally块。finally块:finally块无论如何都会执行,所以会打印“执行finally块”。- 返回值:
finally块执行完毕后,程序才会继续执行catch块中的return语句,最终返回值为1。
关键点总结:
finally块总会执行:无论在try或catch块中是否有return语句,finally块的代码总会执行。- 执行顺序:
finally块的代码执行完毕后,程序才会从try或catch块中的return语句中返回。
特殊情况
如果在finally块中也有return语句,那么finally块中的return将会覆盖try或catch块中的return语句,最终返回finally块中的值。
示例:
1 | public class TryCatchFinallyExample { |
输出结果:
1 | 捕获异常: / by zero |
在这个例子中,由于finally块中的return语句,最终返回的值是2,而不是catch块中的1。因此,应谨慎在finally块中使用return语句,因为它会影响返回值,可能导致意料之外的行为。
进一步解释
在 Java 中,当涉及到try-catch-finally结构时,finally块的设计初衷就是为了确保资源的释放或一些必要的清理操作无论如何都能执行。这种设计导致了finally块的执行优先级非常高,以至于即使在try或catch块中已经出现了return语句,Java也会确保finally块中的代码先执行。
具体执行过
当JVM遇到try-catch-finally结构时,执行顺序和行为如下:
try块:程序首先进入try块,执行其中的代码。catch块:如果try块中发生了异常,程序会跳转到相应的catch块中执行代码。如果catch块中有return语句,JVM会记录下这个返回值(或者跳转目标),但在实际返回之前会先执行finally块。finally块:无论try或catch块中是否有return、break、continue或者抛出异常,finally块中的代码都会执行。- 返回
catch或try块的return值:如果finally块中没有return语句,程序会返回先前在try或catch块中准备好的返回值。 finally块中的**return覆盖:如果finally块中有return语句,这个return会覆盖try或catch块中的任何返回值,直接返回finally块中的值。这是因为finally块是最后执行的代码块,其return语句直接决定了最终返回值。
原因分析:
这种行为的原因与Java的异常处理机制设计有关:
- 资源管理和一致性:
finally块主要用于确保资源的正确释放(如关闭文件、释放锁、关闭数据库连接等),即使在异常或提前返回的情况下也不例外。因此,Java的设计者确保了finally块能够在任何情况下执行,从而保证资源管理的一致性。 - 执行顺序和返回值的确定性:为了避免执行顺序的混乱,Java选择了在
finally块执行后,再决定是否从try或catch块返回结果。这样可以确保在任何情况下,finally块的操作都不会被忽略。 - 覆盖行为:当
finally块中存在return语句时,JVM认为finally块中执行的操作是最终的,所以直接返回finally中的值。这是为了确保finally块的操作是决定性的,这也就导致了try或catch块中的返回值被覆盖。
总结:
finally块的执行优先级高于try和catch块中的返回操作是为了保证代码执行的确定性和资源管理的一致性。当finally块中有return语句时,JVM直接返回finally块中的值,从而覆盖try或catch块中的返回值。这种设计虽然有效地保证了资源管理,但也需要开发人员在编写代码时谨慎使用finally块中的return语句,以避免意外的行为。