异常的概念

什么是异常?

  • 指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RuntimeExceptionExample {
public static void main(String[] args) {
String str = null;

try {
// 尝试调用str的length()方法,这会导致NullPointerException
int length = str.length();
System.out.println("字符串的长度是: " + length);
} catch (NullPointerException e) {
// 捕获并处理NullPointerException
System.out.println("发生了空指针异常: " + e.getMessage());
} finally {
// finally块,始终会执行
System.out.println("这是finally块,无论是否发生异常都会执行。");
}

System.out.println("程序继续运行...");
}
}

输出结果:

1
2
3
发生了空指针异常: Cannot invoke "String.length()" because "str" is null
这是finally块,无论是否发生异常都会执行。
程序继续运行...

代码分析:

  1. String str = null;:我们定义了一个String类型的变量str,并将其初始化为null
  2. try块:我们在try块中尝试调用str.length()方法,因为strnull,所以会抛出NullPointerException
  3. catch块:当NullPointerException发生时,程序跳转到catch块,捕获这个异常,并打印错误信息。这里,e.getMessage()返回异常的详细信息。
  4. finally块:finally块中的代码无论是否发生异常都会执行,用来清理资源或执行一些必要的操作。
  5. 程序继续运行:尽管发生了异常,由于我们在catch块中处理了它,程序能够继续运行,不会崩溃。

示例 2

ArrayIndexOutOfBoundsException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ArrayExceptionExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};

try {
// 访问数组中不存在的索引
int number = numbers[5];
System.out.println("数组中的数字是: " + number);
} catch (ArrayIndexOutOfBoundsException e) {
// 捕获并处理数组越界异常
System.out.println("数组索引越界: " + e.getMessage());
} finally {
System.out.println("数组处理结束。");
}

System.out.println("程序继续运行...");
}
}

输出结果:

1
2
3
数组索引越界: Index 5 out of bounds for length 3
数组处理结束。
程序继续运行...

代码分析:

  1. int[] numbers = {1, 2, 3};:我们定义了一个包含3个整数的数组。
  2. try:尝试访问数组的第6个元素(索引5),这将抛出ArrayIndexOutOfBoundsException,因为数组中只有3个元素。
  3. catch块:捕获并处理ArrayIndexOutOfBoundsException,打印异常信息。
  4. finally块:最后,无论是否发生异常,finally块都会执行,表示数组处理操作结束。
  5. 程序继续运行:捕获并处理异常后,程序继续正常运行。

总结:

  通过捕获和处理运行时异常,程序可以避免在发生错误时直接崩溃,从而提高程序的健壮性和用户体验。虽然Java不强制要求处理运行时异常,但在关键部分处理这些异常是编写健壮代码的好习惯。

编译时异常

  Java 中的编译异常(Checked Exceptions)是必须在编译时处理的异常。它们通常用于表示程序在正常执行流程中可能出现的可预见性错误(如文件未找到、数据库连接失败等)。如果程序没有对这些异常进行处理(通过try-catch块或throws声明),编译器将会报错,无法通过编译。

处理机制

编译异常的处理有两种主要方式:

  1. try-catch块:在方法内部捕获并处理异常。
  2. throws声明:在方法签名中声明抛出异常,将异常抛给调用者处理。

示例 1

try-catch块可以用来捕获和处理编译异常,确保程序在发生错误时能够进行适当的处理。

处理IOException(例如文件未找到异常)的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class CheckedExceptionExample {
public static void main(String[] args) {
BufferedReader reader = null;

try {
// 尝试打开一个文件
reader = new BufferedReader(new FileReader("example.txt"));
String line = reader.readLine();
System.out.println("读取的行: " + line);
} catch (IOException e) {
// 捕获并处理IOException
System.out.println("发生IO异常: " + e.getMessage());
} finally {
// 确保资源被关闭
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
System.out.println("关闭文件时发生异常: " + e.getMessage());
}
}
}
}

输出结果(假设文件不存在):

1
发生IO异常: example.txt (No such file or directory)

代码分析:

  • try块:尝试打开一个名为example.txt的文件并读取其内容。
  • catch块:如果文件不存在或无法读取,将抛出IOExceptioncatch块捕获并处理该异常。
  • finally块:确保文件读取器在操作完成后被正确关闭,避免资源泄漏。

示例 2

另一种处理编译异常的方式是在方法签名中使用throws关键字,将异常抛给方法的调用者处理。

抛出IOException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ThrowsExample {
public static void main(String[] args) {
try {
readFile("example.txt");
} catch (IOException e) {
// 捕获并处理IOException
System.out.println("发生IO异常: " + e.getMessage());
}
}

// 抛出IOException,由调用者处理
public static void readFile(String filePath) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line = reader.readLine();
System.out.println("读取的行: " + line);
reader.close();
}
}

输出结果(假设文件不存在):

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已经提供了大量的内置异常类,但在某些情况下,这些内置异常可能无法准确描述特定的问题或业务场景。自定义异常可以帮助我们更清晰地表达这些特殊情况,从而提高代码的可读性和可维护性。

自定义异常的步骤

  1. 继承Exception类或其子类:
    • 如果希望创建一个受检异常(必须处理的异常),需要继承Exception类。
    • 如果希望创建一个非受检异常(不强制处理的异常),可以继承RuntimeException类。
  2. 提供构造方法
    • 通常会提供一个默认的无参构造方法,以及一个可以接受异常信息的构造方法,方便在抛出异常时传递详细信息。
  3. 抛出自定义异常
    • 在程序中适当的位置使用throw关键字抛出自定义异常。

自定义受检异常

以下示例展示了如何创建一个自定义的受检异常InvalidAgeException,用于处理用户年龄不合法的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 自定义异常类,继承Exception
class InvalidAgeException extends Exception {
// 默认构造方法
public InvalidAgeException() {
super("年龄不合法");
}

// 接受自定义错误信息的构造方法
public InvalidAgeException(String message) {
super(message);
}
}

public class CustomExceptionExample {
// 检查年龄的方法,可能会抛出InvalidAgeException
public static void checkAge(int age) throws InvalidAgeException {
if (age < 18 || age > 100) {
throw new InvalidAgeException("年龄必须在18到100岁之间。");
}
System.out.println("年龄合法: " + age);
}

public static void main(String[] args) {
try {
// 调用方法并传入一个不合法的年龄
checkAge(15);
} catch (InvalidAgeException e) {
// 捕获并处理自定义异常
System.out.println("捕获到异常: " + e.getMessage());
}
}
}

输出结果:

1
捕获到异常: 年龄必须在18100岁之间。

自定义非受检异常

以下示例展示了如何创建一个自定义的非受检异常InsufficientFundsException,用于处理银行账户余额不足的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 自定义异常类,继承RuntimeException
class InsufficientFundsException extends RuntimeException {
// 默认构造方法
public InsufficientFundsException() {
super("账户余额不足");
}

// 接受自定义错误信息的构造方法
public InsufficientFundsException(String message) {
super(message);
}
}

public class BankAccount {
private double balance;

public BankAccount(double balance) {
this.balance = balance;
}

// 提款方法,可能会抛出InsufficientFundsException
public void withdraw(double amount) {
if (amount > balance) {
throw new InsufficientFundsException("您的账户余额不足,无法提取: " + amount);
}
balance -= amount;
System.out.println("成功提取: " + amount + ",当前余额: " + balance);
}

public static void main(String[] args) {
BankAccount account = new BankAccount(1000.0);

try {
// 尝试提取超过余额的金额
account.withdraw(1500.0);
} catch (InsufficientFundsException e) {
// 捕获并处理自定义异常
System.out.println("捕获到异常: " + e.getMessage());
}
}
}

输出结果:

1
捕获到异常: 您的账户余额不足,无法提取: 1500.0

代码分析

  • InvalidAgeExceptionInsufficientFundsException** 是两个自定义异常类,分别继承了ExceptionRuntimeException
  • 这两个类都提供了默认构造方法和接受自定义错误信息的构造方法,使得在抛出异常时可以提供详细的错误描述。
  • BankAccount类和CustomExceptionExample类中,通过检查条件,如果不满足要求,抛出自定义异常,并在main方法中通过try-catch块捕获这些异常,进行相应的处理。

自定义异常的好处

  1. 表达清晰:自定义异常可以更准确地表达特定的业务错误,使代码更加清晰易懂。
  2. 增强可维护性:通过自定义异常,可以将特定错误情景与普通逻辑代码分离,提高代码的可维护性。
  3. 统一异常处理:在大规模应用中,可以统一处理某类特定的业务异常,从而提高异常处理的一致性和代码质量。

通过合理使用自定义异常,可以使 Java 应用程序的异常处理机制更加灵活和高效。

其他

在 Java 中,当使用try-catch-finally结构时,即使catch块中有return语句,finally块中的代码仍然会执行,而且是在catch块中的return语句执行之前。也就是说,finally块的执行优先级高于catch块中的return

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TryCatchFinallyExample {
public static void main(String[] args) {
System.out.println("返回值: " + testMethod());
}

public static int testMethod() {
try {
int result = 10 / 0; // 这里会抛出异常
return result;
} catch (ArithmeticException e) {
System.out.println("捕获异常: " + e.getMessage());
return 1; // catch块中有return语句
} finally {
System.out.println("执行finally块");
}
}
}

输出结果:

1
2
3
捕获异常: / by zero
执行finally
返回值: 1

代码分析:

  1. try块:尝试执行10 / 0,会抛出ArithmeticException,所以直接跳转到catch块,result的值不会被返回。
  2. catch块:捕获到ArithmeticException,打印“捕获异常: / by zero”,并且遇到return 1;准备返回值1。但在真正返回值之前,Java会先执行finally块。
  3. finally块:finally块无论如何都会执行,所以会打印“执行finally块”。
  4. 返回值:finally块执行完毕后,程序才会继续执行catch块中的return语句,最终返回值为1。

关键点总结:

  • finally块总会执行:无论在trycatch块中是否有return语句,finally块的代码总会执行。
  • 执行顺序:finally块的代码执行完毕后,程序才会从trycatch块中的return语句中返回。

特殊情况

  如果在finally块中也有return语句,那么finally块中的return将会覆盖trycatch块中的return语句,最终返回finally块中的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TryCatchFinallyExample {
public static void main(String[] args) {
System.out.println("返回值: " + testMethod());
}

public static int testMethod() {
try {
int result = 10 / 0;
return result;
} catch (ArithmeticException e) {
System.out.println("捕获异常: " + e.getMessage());
return 1;
} finally {
System.out.println("执行finally块");
return 2; // 覆盖之前的返回值
}
}
}

输出结果:

1
2
3
捕获异常: / by zero
执行finally
返回值: 2

在这个例子中,由于finally块中的return语句,最终返回的值是2,而不是catch块中的1。因此,应谨慎在finally块中使用return语句,因为它会影响返回值,可能导致意料之外的行为。

进一步解释

  在 Java 中,当涉及到try-catch-finally结构时,finally块的设计初衷就是为了确保资源的释放或一些必要的清理操作无论如何都能执行。这种设计导致了finally块的执行优先级非常高,以至于即使在trycatch块中已经出现了return语句,Java也会确保finally块中的代码先执行。

具体执行过

当JVM遇到try-catch-finally结构时,执行顺序和行为如下:

  1. try块:程序首先进入try块,执行其中的代码。
  2. catch块:如果try块中发生了异常,程序会跳转到相应的catch块中执行代码。如果catch块中有return语句,JVM会记录下这个返回值(或者跳转目标),但在实际返回之前会先执行finally块。
  3. finally块:无论trycatch块中是否有returnbreakcontinue或者抛出异常,finally块中的代码都会执行。
  4. 返回catchtry块的return值:如果finally块中没有return语句,程序会返回先前在trycatch块中准备好的返回值。
  5. finally块中的**return覆盖:如果finally块中有return语句,这个return会覆盖trycatch块中的任何返回值,直接返回finally块中的值。这是因为finally块是最后执行的代码块,其return语句直接决定了最终返回值。

原因分析:

这种行为的原因与Java的异常处理机制设计有关:

  • 资源管理和一致性finally块主要用于确保资源的正确释放(如关闭文件、释放锁、关闭数据库连接等),即使在异常或提前返回的情况下也不例外。因此,Java的设计者确保了finally块能够在任何情况下执行,从而保证资源管理的一致性。
  • 执行顺序和返回值的确定性:为了避免执行顺序的混乱,Java选择了在finally块执行后,再决定是否从trycatch块返回结果。这样可以确保在任何情况下,finally块的操作都不会被忽略。
  • 覆盖行为:当finally块中存在return语句时,JVM认为finally块中执行的操作是最终的,所以直接返回finally中的值。这是为了确保finally块的操作是决定性的,这也就导致了trycatch块中的返回值被覆盖。

总结:

  finally块的执行优先级高于trycatch块中的返回操作是为了保证代码执行的确定性和资源管理的一致性。当finally块中有return语句时,JVM直接返回finally块中的值,从而覆盖trycatch块中的返回值。这种设计虽然有效地保证了资源管理,但也需要开发人员在编写代码时谨慎使用finally块中的return语句,以避免意外的行为。