应对意外情况时的优雅处理
1. 计算机异常历史回顾
在计算机发展历程中,出现过许多因小失误导致大问题的情况:
- 1945年9月9日,一只飞蛾飞进哈佛Mark II计算机的一个继电器中,导致机器故障,这成为有记录以来第一个真正的计算机“bug”。
- 1957年4月19日,赫伯特·布莱特收到一盒未标记的计算机穿孔卡片,他猜测这来自FORTRAN开发团队。他编写了一个FORTRAN程序并在IBM 704上编译,最初因一个语句中缺少逗号而报错,修正后程序正常运行。
- 1962年7月22日,美国第一艘飞往其他行星的航天器水手一号在发射四分钟后出现异常并被摧毁,原因是火箭速度公式中缺少一个类似连字符的符号。同时,NASA的轨道计算软件中存在错误语句,将循环语句写成了赋值语句。
- 2000年1月1日,“千年虫问题”给现代世界带来了严重破坏。
2. 异常处理基础
假设你正在进行库存盘点,要输入大灰尘箱的数量,每个箱子价值3.25美元。以下是一个简单的库存处理程序:
import static java.lang.System.out;
import java.util.Scanner;
import java.text.NumberFormat;
public class InventoryA {
public static void main(String args[]) {
final double boxPrice = 3.25;
Scanner keyboard = new Scanner(System.in);
NumberFormat currency = NumberFormat.getCurrencyInstance();
out.print("How many boxes do we have? ");
String numBoxesIn = keyboard.next();
int numBoxes = Integer.parseInt(numBoxesIn);
out.print("The value is ");
out.println(currency.format(numBoxes * boxPrice));
keyboard.close();
}
}
当用户输入整数时,程序正常运行;但输入非整数(如3.5)时,程序会崩溃,并抛出
java.lang.NumberFormatException
异常。这是因为
Integer.parseInt
方法在处理无法解析为整数的字符串时会抛出该异常。
Java语言有异常处理机制,当程序检测到即将出现问题时,会创建一个新的异常对象,即“抛出异常”。异常对象会在代码中传递,直到被某个代码块“捕获”。Java中用于异常处理的关键字有:
-
throw
:创建一个新的异常对象。
-
throws
:将异常责任从一个方法传递给调用该方法的代码。
-
try
:包含可能抛出异常的代码。
-
catch
:处理捕获到的异常,然后继续执行后续代码。
为了修复上述程序的问题,可以使用
try-catch
语句:
import static java.lang.System.out;
import java.util.Scanner;
import java.text.NumberFormat;
public class InventoryB {
public static void main(String args[]) {
final double boxPrice = 3.25;
Scanner keyboard = new Scanner(System.in);
NumberFormat currency = NumberFormat.getCurrencyInstance();
out.print("How many boxes do we have? ");
String numBoxesIn = keyboard.next();
try {
int numBoxes = Integer.parseInt(numBoxesIn);
out.print("The value is ");
out.println(currency.format(numBoxes * boxPrice));
} catch (NumberFormatException e) {
out.println("That's not a number.");
}
keyboard.close();
}
}
在这个程序中,将
Integer.parseInt
方法调用放在
try
块中,当抛出
NumberFormatException
异常时,程序会跳转到
catch
块中执行相应的处理代码。
3. catch子句参数
catch
子句中的参数类似于方法的参数列表,它包含异常类型和一个参数名。例如:
} catch (NumberFormatException e) {
out.println("Message: ***" + e.getMessage() + "***");
e.printStackTrace();
}
当捕获到异常时,参数
e
会引用该异常对象。通过调用异常对象的方法(如
getMessage
和
printStackTrace
),可以获取异常的详细信息,帮助定位问题。
4. 异常类型
除了
NumberFormatException
,Java中还有许多其他类型的异常,甚至可以自定义异常类。以下是自定义异常类和使用该异常类的示例:
@SuppressWarnings("serial")
class OutOfRangeException extends Exception {
}
import static java.lang.System.out;
import java.util.Scanner;
import java.text.NumberFormat;
public class InventoryC {
public static void main(String args[]) {
final double boxPrice = 3.25;
Scanner keyboard = new Scanner(System.in);
NumberFormat currency = NumberFormat.getCurrencyInstance();
out.print("How many boxes do we have? ");
String numBoxesIn = keyboard.next();
try {
int numBoxes = Integer.parseInt(numBoxesIn);
if (numBoxes < 0) {
throw new OutOfRangeException();
}
out.print("The value is ");
out.println(currency.format(numBoxes * boxPrice));
} catch (NumberFormatException e) {
out.println("That's not a number.");
} catch (OutOfRangeException e) {
out.print(numBoxesIn);
out.println("? That's impossible!");
}
keyboard.close();
}
}
在库存程序中,当用户输入负数时,会抛出
OutOfRangeException
异常,程序会给出相应的提示。
5. 异常匹配规则
一个
try
块可以有多个
catch
块。当异常抛出时,计算机从第一个
catch
块开始检查,判断抛出的异常是否是该
catch
块参数类型的实例。如果是,则执行该
catch
块的代码,并跳过后续的
catch
块;如果不是,则继续检查下一个
catch
块。
例如,以下代码展示了多个
catch
块的情况:
import static java.lang.System.out;
import java.util.Scanner;
import java.text.NumberFormat;
public class InventoryD {
public static void main(String args[]) {
final double boxPrice = 3.25;
Scanner keyboard = new Scanner(System.in);
NumberFormat currency = NumberFormat.getCurrencyInstance();
out.print("How many boxes do we have? ");
String numBoxesIn = keyboard.next();
try {
int numBoxes = Integer.parseInt(numBoxesIn);
if (numBoxes < 0) {
throw new OutOfRangeException();
}
if (numBoxes > 1000) {
throw new NumberTooLargeException();
}
out.print("The value is ");
out.println(currency.format(numBoxes * boxPrice));
}
catch (NumberFormatException e) {
out.println("That's not a number.");
}
catch (OutOfRangeException e) {
out.print(numBoxesIn);
out.println("? That's impossible!");
}
catch (Exception e) {
out.print("Something went wrong, ");
out.print("but I'm clueless about what ");
out.println("it actually was.");
}
out.println("That's that.");
keyboard.close();
}
}
在这个程序中,
NumberTooLargeException
是
OutOfRangeException
的子类,当抛出
NumberTooLargeException
异常时,会被
catch (OutOfRangeException e)
块捕获。
以下是不同输入情况下程序的处理流程:
| 用户输入 | 抛出异常 | 匹配的catch块 | 处理结果 |
| — | — | — | — |
| 普通整数(如3) | 无 | 无 | 执行try块内所有语句,跳过所有catch块,执行后续代码 |
| 非整数(如fish) | NumberFormatException | catch (NumberFormatException e) | 跳过try块剩余语句,执行该catch块代码,跳过后续catch块,执行后续代码 |
| 负数(如 -25) | OutOfRangeException | catch (OutOfRangeException e) | 跳过try块剩余语句,跳过第一个catch块,执行该catch块代码,跳过后续catch块,执行后续代码 |
| 过大的数(如1001) | NumberTooLargeException | catch (OutOfRangeException e) | 跳过try块剩余语句,跳过第一个catch块,执行该catch块代码,跳过后续catch块,执行后续代码 |
| 其他不可预测异常(如IOException) | IOException | catch (Exception e) | 跳过try块剩余语句,跳过前两个catch块,执行该catch块代码,执行后续代码 |
mermaid流程图如下:
graph TD;
A[用户输入] --> B{输入是否为整数};
B -- 是 --> C{是否为负数};
C -- 是 --> D(抛出OutOfRangeException);
C -- 否 --> E{是否大于1000};
E -- 是 --> F(抛出NumberTooLargeException);
E -- 否 --> G(正常计算并输出结果);
B -- 否 --> H(抛出NumberFormatException);
D --> I(匹配catch OutOfRangeException);
F --> I;
H --> J(匹配catch NumberFormatException);
I --> K(输出相应提示,继续执行后续代码);
J --> K;
L(其他不可预测异常) --> M(抛出异常);
M --> N(匹配catch Exception);
N --> K;
应对意外情况时的优雅处理
6. 一次性捕获多种异常
从Java 7开始,可以在一个
catch
子句中捕获多种类型的异常。例如,在库存程序中,如果不想区分
NumberFormatException
和
OutOfRangeException
,可以这样编写代码:
try {
int numBoxes = Integer.parseInt(numBoxesIn);
if (numBoxes < 0) {
throw new OutOfRangeException();
}
if (numBoxes > 1000) {
throw new NumberTooLargeException();
}
out.print("The value is ");
out.println(currency.format(numBoxes * boxPrice));
}
catch (NumberFormatException | OutOfRangeException e) {
out.print(numBoxesIn);
out.println("? That's impossible!");
}
catch (Exception e) {
out.print("Something went wrong, ");
out.print("but I'm clueless about what ");
out.println("it actually was.");
}
使用竖线
|
来分隔不同的异常类型,当抛出
NumberFormatException
或
OutOfRangeException
时,程序会执行相应的
catch
块代码。
7. 避免不必要的异常捕获
Java不允许捕获不可能抛出的异常。例如,以下代码会引发编译器错误:
// Bad code!
try {
i++;
} catch (IOException e) {
e.printStackTrace();
}
因为
i++
语句不涉及任何输入输出操作,所以
try
块内的代码不可能抛出
IOException
异常,编译器会给出错误提示:
exception java.io.IOException is never thrown
in body of corresponding try statement
8. 异常捕获后的持续处理
前面的示例大多是捕获异常后打印错误信息并结束程序。实际上,可以让程序在捕获异常后继续运行。以下是一个在循环中使用
try-catch
语句的示例:
import static java.lang.System.out;
import java.util.Scanner;
import java.text.NumberFormat;
public class InventoryLoop {
public static void main(String args[]) {
final double boxPrice = 3.25;
boolean gotGoodInput = false;
Scanner keyboard = new Scanner(System.in);
NumberFormat currency = NumberFormat.getCurrencyInstance();
do {
out.print("How many boxes do we have? ");
String numBoxesIn = keyboard.next();
try {
int numBoxes = Integer.parseInt(numBoxesIn);
out.print("The value is ");
out.println(currency.format(numBoxes * boxPrice));
gotGoodInput = true;
} catch (NumberFormatException e) {
out.println();
out.println("That's not a number.");
}
} while (!gotGoodInput);
out.println("That's that.");
keyboard.close();
}
}
在这个程序中,使用
do-while
循环,只要用户输入的不是有效的整数,循环就会继续,直到用户输入正确的整数为止。
以下是该程序的执行步骤列表:
1. 初始化变量
gotGoodInput
为
false
。
2. 进入
do-while
循环,提示用户输入箱子数量。
3. 获取用户输入,尝试将其转换为整数。
4. 如果转换成功,计算并输出箱子的价值,将
gotGoodInput
设置为
true
。
5. 如果转换失败,捕获
NumberFormatException
异常,输出错误提示,继续循环。
6. 当
gotGoodInput
为
true
时,退出循环,输出结束信息。
9. 正常情况下的异常使用
并非所有的异常都是由错误情况引起的,有些异常是正常预期的情况。例如,在文件复制过程中,检测到文件末尾时会抛出
EOFException
异常:
try {
while (true) {
dataOut.writeByte(dataIn.readByte());
}
} catch (EOFException e) {
numFilesCopied = 1;
}
在这个示例中,使用
while (true)
创建一个看似无限的循环,当读取到文件末尾时,
readByte
方法会抛出
EOFException
异常,程序会跳出循环并执行
catch
块中的代码。
mermaid流程图如下:
graph TD;
A[开始文件复制] --> B{是否到达文件末尾};
B -- 否 --> C(读取并写入字节);
C --> B;
B -- 是 --> D(抛出EOFException);
D --> E(捕获异常,复制文件数加1);
E --> F[结束]
综上所述,Java的异常处理机制为我们提供了强大的工具来处理程序中可能出现的意外情况。通过合理使用
try-catch
语句、自定义异常类和异常匹配规则,可以让程序更加健壮和可靠。同时,要注意避免不必要的异常捕获,并且可以让程序在捕获异常后继续执行有用的操作。