第八章 异常处理

1 认识 Java 的异常

1.1、什么是异常

​ 在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式问题,读取文件是否存在,网络是否始终保持通畅等等。

  • 异常 :指的是程序在执行过程中,出现的非正常的情况,如果不处理最终会导致 JVM 的非正常停止。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。

异常也不是指逻辑代码错误而没有得到想要的结果,例如:求a与b的和,你写成了a-b

1.2、如何对待异常

程序员在编写程序时,就应该充分考虑到各种可能发生的异常和错误,极力预防和避免,实在无法避免的,要编写相应的代码进行异常的检测、异常消息的提示,以及异常的处理。

1.3、异常的抛出机制

Java 中是如何表示不同的异常情况,又是如何让程序员得知,并处理异常的呢?

Java 中把不同的异常用不同的类表示,一旦发生某种异常,就通过创建该异常类型的对象,并且抛出,然后程序员可以 catch 到这个异常对象,并处理,如果无法 catch 到这个异常对象,那么这个异常对象将会导致程序终止。

运行下面的程序,程序会产生一个数组索引越界异常 ArrayIndexOfBoundsException 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}

public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
intnum = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}

​ Java 运行时系统会沿着调用栈向上查找,看是否有代码块可以处理这个异常。这个查找过程从抛出异常的方法开始,一直向上直到 main 方法。如果有可以处理异常的代码块,就把异常对象交给这个代码块处理。这个代码块被称为“异常处理器”或“catch 块”。

1.4、异常的继承体系

​ Java 的异常继承体系是基于Java类继承机制构建的,所有的异常类都继承自java.lang.Throwable类。Throwable类有两个主要的直接子类:java.lang.Errorjava.lang.Exception

  1. java.lang.Error:这是Java运行时系统内部错误和资源耗尽错误,比如OutOfMemoryError(内存溢出错误)和StackOverflowError(栈溢出错误)等。这些错误通常是Java虚拟机(JVM)无法或不应该尝试捕获的严重问题。

  2. java.lang.Exception:这是应用程序需要捕获并处理的异常。Exception类及其子类表示程序本身可以预见的、可以处理的问题,比如IOException(输入/输出异常)、SQLException(SQL异常)等。

Exception类进一步细分为两类:

  • 检查型异常(Checked Exceptions):这是编译器要求必须捕获或声明的异常。检查型异常通常表示程序员的错误,比如文件找不到、网络不可用等。常见的检查型异常有IOExceptionClassNotFoundException等。

  • 运行时异常(Runtime Exceptions):这是编译器不要求必须捕获的异常,它们通常是程序运行时的逻辑错误,比如NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组越界异常)等。运行时异常是RuntimeException类或其子类的实例。

在 Java 的异常处理中,使用try块来包含可能抛出异常的代码,catch 块来捕获并处理异常,finally 块来包含无论是否发生异常都需要执行的代码。此外,还可以使用 throw 关键字来显式抛出异常,以及使用 throws 关键字在方法签名中声明方法可能抛出的异常。

2 异常的处理

2.1 概述

在 Java 中,处理异常通常涉及到使用try, catch, finally, 和 throw 关键字。下面是如何处理异常的几个步骤:

  1. 使用 try 块来包围可能抛出异常的代码
    当你预期某个代码块可能会抛出异常时,你应该将该代码块放在try块中。

  2. 使用 catch 块来捕获并处理异常
    catch块紧跟在try块之后,用于捕获try块中抛出的异常。你可以指定要捕获的异常类型,并在catch块中处理该异常。

  3. 使用多个 catch 块来处理不同类型的异常
    你可以使用多个catch块来捕获和处理不同类型的异常。每个catch块应该处理一种特定类型的异常。

  4. 使用 finally 块来执行清理操作
    finally块是可选的,它包含的代码无论是否发生异常都会被执行。通常用于资源清理,如关闭文件、数据库连接等。

  5. 使用 throw 关键字来抛出异常
    如果你认为某个方法无法处理特定的异常,你可以使用throw关键字来抛出该异常,将其传递给调用者处理。

下面是一个简单的异常处理示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExceptionHandlingExample {
public static void main(String[] args) {
try {
// 尝试执行可能抛出异常的代码
int result = divide(10, 0);
System.out.println("结果是: " + result);
} catch (ArithmeticException e) {
// 捕获并处理特定类型的异常
System.out.println("发生了除零异常: " + e.getMessage());
} finally {
// 清理资源或执行无论是否发生异常都需要执行的代码
System.out.println("这是finally块");
}
}

public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("除数不能为零");
}
return a / b;
}
}

​ 在这个示例中,divide 方法会检查除数是否为零,如果是,则抛出一个ArithmeticException。在main方法中,我们尝试调用divide方法,并使用try-catch块来捕获可能抛出的异常。finally块确保无论是否发生异常,都会执行清理代码。

​ 注意,如果异常没有在try-catch块中被捕获,它将被传递给调用者,直到被捕获或最终由Java运行时系统处理。如果异常类型匹配某个catch块,那么该catch块的代码将被执行,异常得到处理。如果没有catch块匹配抛出的异常类型,或者没有catch块来处理异常,程序将终止,并打印堆栈跟踪信息。

2.2 try…catch…

捕获异常语法如下:

1
2
3
4
5
6
7
8
try{
可能发生xx异常的代码
} catch(异常类型1 e){
处理异常的代码1
} catch(异常类型2 e){
处理异常的代码2
}
....

try{} 中编写可能发生某些异常的业务逻辑代码。

catch 分支,分为两个部分,catch() 中编写异常类型和异常参数名,{}中编写如果发生了这个异常,要做什么处理的代码。如果有多个 catch 分支,并且多个异常类型有父子类关系,必须保证小的子异常类型在上,大的父异常类型在下。

当某段代码可能发生异常,不管这个异常是编译时异常(受检异常)还是运行时异常(非受检异常),我们都可以使用 try 块将它括起来,并在 try 块下面编写 catch 分支尝试捕获对应的异常对象。

  • 如果在程序运行时,try 块中的代码没有发生异常,那么 catch 所有的分支都不执行。
  • 如果在程序运行时,try 块中的代码发生了异常,根据异常对象的类型,将从上到下选择第一个匹配的catch 分支执行。此时 try 中发生异常的语句下面的代码将不执行,而整个 try…catch 之后的代码可以继续运行。
  • 如果在程序运行时,try块中的代码发生了异常,但是所有 catch 分支都无法匹配(捕获)这个异常,那么 JVM 将会终止当前方法的执行,并把异常对象“抛”给调用者。如果调用者不处理,程序就挂了。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestTryCatch {
public static void main(String[] args) {
try {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int result = a/b;
System.out.println("result = " + result);
} catch (NumberFormatException e) {
System.out.println("数字格式不正确,请输入两个整数");
} catch (ArrayIndexOutOfBoundsException e){
System.out.println("数字个数不正确,请输入两个整数");
} catch (ArithmeticException e){
System.out.println("第二个整数不能为0");
}

System.out.println("你输入了" + args.length +"个参数。");
System.out.println("你输入的被除数和除数分别是:");
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]+" ");
}
System.out.println();
}
}

2.3 finally

​ 因为异常会引发程序跳转,从而会导致有些语句执行不到。而程序中有一些特定的代码无论异常是否发生,都需要执行。例如,IO流的关闭,数据库连接的断开等。这样的代码通常就会放到 finally 块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try{

}catch(...){

}finally{
无论try中是否发生异常,也无论catch是否捕获异常,也不管trycatch中是否有return语句,都一定会执行
}


try{

}finally{
无论try中是否发生异常,也不管try中是否有return语句,都一定会执行。
}

注意: finally 不能单独使用。

当只有在 try 或者 catch 中调用退出 JVM 的相关方法,例如 System.exit(0),此时 finally 才不会执行,否则 finally 永远会执行。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.InputMismatchException;
import java.util.Scanner;

public class TestFinally {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("请输入第一个整数:");
int a = input.nextInt();
System.out.print("请输入第二个整数:");
int b = input.nextInt();
int result = a/b;
System.out.println(a + "/" + b +"=" + result);
} catch (InputMismatchException e) {
System.out.println("数字格式不正确,请输入两个整数");
}catch (ArithmeticException e){
System.out.println("第二个整数不能为0");
} finally {
System.out.println("程序结束,释放资源");
input.close();
}
}
}

finally 与 return 的关系

在 Java 中,finally 块与 return 语句之间的关系是一个常见的误解点。让我们首先明确这两者是如何工作的,然后详细讨论它们之间的关系。

  1. finally
  • finally 块是 try-catch-finally 结构的一部分。
  • 无论 try 块或 catch 块中的代码是否抛出异常或正常执行完毕,finally 块中的代码总是会被执行。
  • 这意味着,即使 trycatch 块中有 return 语句,finally 块中的代码也会执行。
  1. return 语句
  • return 语句用于从方法中返回一个值。
  • return 语句被执行时,方法会立即结束,并返回指定的值(如果有的话)。

现在,让我们讨论 finally 块和 return 语句之间的关系:

  • **在 trycatch 块中的 return**:

当你在 trycatch 块中使用 return 语句时,方法会开始退出过程。这意味着,在方法退出之前,finally 块会被执行。

这意味着,finally 块中的任何代码都可能改变 return 语句返回的值。这通常是不推荐的,因为它可能会导致代码难以理解和维护。但是,Java 确实允许这样做。

1
2
3
4
5
6
7
public int exampleMethod() {
try {
return 10;
} finally {
return 20; // This will override the return value from the try block
}
}

在上述示例中,尽管 try 块尝试返回 10,但由于 finally 块中的 return 20,方法最终返回 20

  • **在 finally 块中的 return**:

如果在 finally 块中使用了 return 语句,那么它会覆盖 trycatch 块中的任何 return 语句。这意味着,无论 trycatch 块中的代码如何,finally 块中的 return 语句都会决定方法的最终返回值。

1
2
3
4
5
6
7
8
9
public int exampleMethod() {
try {
return 10; // This return value will be overridden by the finally block
} catch (Exception e) {
return 20; // This return value will also be overridden by the finally block
} finally {
return 30; // This is the value that will be returned by the method
}
}

在上述示例中,无论 trycatch 块中的代码如何,方法最终都会返回 30,因为 finally 块中的 return 语句决定了最终的结果。

总之,finally 块和 return 语句之间的关系是,finally 块总是会被执行,并且它中的 return 语句可以覆盖 trycatch 块中的 return 语句。因此,在设计代码时,应该小心处理这种关系,以避免意外的行为或难以追踪的错误。

2.4 throw 与 throws

在 Java 中,抛出异常通常涉及两个步骤:首先,你需要创建一个异常对象,然后,你需要使用 throw 关键字来抛出这个异常对象。异常通常是由程序中的错误或异常情况触发的,它们可以是由 Java 运行时环境自动抛出的,也可以是由程序员显式抛出的。

下面是一个简单的例子,展示了如何在 Java 中抛出异常:

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
public class CustomExceptionExample {
public static void main(String[] args) {
try {
// 调用可能抛出异常的方法
methodThatThrowsException();
} catch (CustomException e) {
// 处理异常
System.out.println("捕获到异常: " + e.getMessage());
}
}

// 一个可能抛出异常的方法
public static void methodThatThrowsException() throws CustomException {
// 检查某种条件
boolean condition = false;
if (!condition) {
// 创建一个异常对象
CustomException exception = new CustomException("这是一个自定义异常");
// 抛出异常
throw exception;
}
// 其他代码...
}

// 自定义异常类,继承自 Exception 或 RuntimeException
static class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
}
}

在这个例子中,methodThatThrowsException 方法可能会抛出一个 CustomException。当 condition 不满足时,我们创建了一个 CustomException 对象,并使用 throw 关键字将其抛出。

注意,如果一个方法可能会抛出异常,但又不处理它,那么该方法必须使用 throws 关键字来声明它可能会抛出的异常。这样,调用这个方法的代码就知道需要处理或继续向上抛出这个异常。

在上面的 main 方法中,我们调用了 methodThatThrowsException 并使用 try-catch 块来处理 CustomException。如果 methodThatThrowsException 抛出了异常,异常将被捕获并打印出异常信息。

此外,异常类通常继承自 java.lang.Exception(表示检查型异常)或 java.lang.RuntimeException(表示未检查型异常)。自定义异常类可以根据需要选择继承自这两者之一。

区别

在 Java 中,throwthrows 是两个与异常处理相关的关键字,它们之间有着明显的区别:

  1. 用途

    • throw 关键字用于在方法内部显式地抛出一个异常对象。它表示一个具体的异常抛出动作,用于在代码中主动抛出异常。
    • throws 关键字则用于在方法签名中声明该方法可能会抛出的异常类型。它表示一种状态,告诉方法的调用者该方法可能会抛出哪些异常,让调用者做好相应的异常处理准备。
  2. 位置

    • throw 关键字出现在方法体内部,用于在适当的时候抛出异常。
    • throws 关键字出现在方法头(函数头)中,紧跟在方法名后面,用于声明方法可能抛出的异常类型。
  3. 异常处理

    • throw 关键字后面通常跟着一个异常对象,表示抛出该异常对象。执行到 throw 语句时,程序会立即停止执行当前方法,并将异常对象传递给调用者处理。
    • throws 关键字用于声明方法可能会抛出的异常类型,但它不会直接抛出异常。它只是一种声明,告诉方法的调用者该方法可能会抛出哪些异常,具体的异常处理由调用者负责。
  4. 异常类型

    • throw 可以抛出任何类型的异常对象,包括自定义异常。
    • throws 用于声明方法可能会抛出的异常类型,可以声明一个或多个异常类型,多个异常类型之间用逗号分隔。

总结来说,throw 是用于在方法内部具体抛出异常的关键字,而 throws 是用于在方法签名中声明该方法可能会抛出的异常类型的关键字。在编写 Java 程序时,根据具体需求选择使用 throw 还是 throws,以确保异常得到妥善处理。