程序在运行过程中,难免发生意外,这种程序中的错误被称之为异常。
异常发生的原因有很多,通常包含以下几大类:
- 用户操作失误,如输入了非法数据,给数字处理程序传入了字母。
- 系统运行错误,操作空对象,出现空指针。
- 外界物理因素,网络通信时连接中断,或者内存不足。
异常处理的必要性
恰当的异常处理可以处理错误并使程序继续执行,从而为用户提供良好的体验。
我们时常会写类似下面的代码:
private static void npeException(String name){
if(name.equals("Tom")){
System.out.println("这是 Tom");
}
}
在理想化的环境中,代码可以正常工作。
但是,如果 name = null ,生产中会发生什么?
Exception in thread "main" java.lang.NullPointerException
at com.mapull.exception.CommonException.npeException(CommonException.java:13)
at com.mapull.exception.CommonException.main(CommonException.java:9)
我们期待的结果没有输出,原本健康的程序反而会完全停止运行!控制台打印了堆栈跟踪记录,我们可以通过控制台打印的这么一大堆信息里搜寻有问题的代码在哪一行发生,从而快速定位到有问题的代码片段。
异常类层级结构
所有的异常都有一个共同的祖先 Throwable,由它衍生的 Exception 我们称之为异常,一般我们的说的 Java 异常主要是这个分支下的,Error 称之为错误,一般无法处理,程序一般不会从错误中恢复。
异常情况主要分为三类:
- 受检查异常(检查性异常)/非运行时异常
- 未经检查的异常(非检查性异常)/运行时异常
- 错误
运行时和未经检查的异常指的是同一件事,我们经常可以互换使用它们。
检查异常 Checked
已检查异常是 Java 编译器要求我们处理的异常。我们必须要么以声明方式将异常抛出调用堆栈,要么我们必须自己处理它。
当我们期望调用的方法的能够恢复时,使用检查异常。
未经检查的异常 Unchecked
未经检查的异常是 Java 编译器不需要我们处理的异常。
简单地说,如果我们创建了一个继承 RuntimeException 的异常 ,它就是一个未经检查异常。
错误 Errors
错误表示严重且通常无法恢复的情况,例如库不兼容、无限递归或内存泄漏。
异常处理 catch
在 Java API 中,有很多地方可能会出错,其中一些地方标有异常,例如方法签名中或者在 Javadoc 中:
/**
* @exception FileNotFoundException ...
*/
public Scanner(String fileName) throws FileNotFoundException {
// ...
}
异常重新抛出 throws
“处理”异常的最简单方法是重新抛出它:
public int getPlayerScore(String playerFile) throws FileNotFoundException {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
}
因为 FileNotFoundException
是一个检查异常,这是满足编译器的最简单方法,但这确实意味着任何调用该方法的人也需要处理它!
parseInt
可以抛出 NumberFormatException
,但因为它是非检查性异常,所以我们不需要处理它。
异常捕获 try-catch
如果我们想自己尝试处理异常,我们可以使用 try-catch块。我们可以通过重新抛出异常来处理它:
public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile) {
throw new IllegalArgumentException("File not found");
}
}
或者通过执行恢复步骤:
public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch ( FileNotFoundException noFile ) {
logger.warn("File not found, resetting score.");
return 0;
}
}
finally
有时,我们需要某些代码一定要被执行,无论是否发生异常,就需要用到 finally。
程序中,可能需要打开文件,或者一个 IO 流,然后对其中的内容处理。但是在处理过程中,难免出错,而我们希望无论处理结果如何,都要将资源关闭,清理工作就适合放到 finally 中。
public int getPlayerScore(String playerFile)
throws FileNotFoundException {
Scanner contents = null;
try {
contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} finally {
if (contents != null) {
contents.close();
}
}
}
在这里,finally
块中是我们希望程序一定会运行的 Java 代码,而不管尝试读取文件时会发生什么。
即使发生 FileNotFoundException
,Java 也会在返回之前调用 finally 的内容。
我们还可以处理异常并确保我们的资源被关闭:
public int getPlayerScore(String playerFile) {
Scanner contents;
try {
contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile ) {
logger.warn("File not found, resetting score.");
return 0;
} finally {
try {
if (contents != null) {
contents.close();
}
} catch (IOException io) {
logger.error("Couldn't close the reader!", io);
}
}
}
因为close也是一个“有风险”的方法,所以我们还需要捕获它的异常!
这可能看起来很复杂,但我们需要处理每个未来可能发生的潜在问题。
try-with-resources
上面的例子是几个经典的资源类处理场景,我们真实的业务场景只有两行代码,却为了处理异常而多出了十多行。从 Java 7 开始,JDK 引入了一种资源自动关闭的简化写法:
语法
try (resource declaration) {
// 使用的资源
} catch (ExceptionType e1) {
// 异常块
}
示例
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException e ) {
logger.warn("File not found, resetting score.");
return 0;
}
}
我们在 try 声明中放置需要自动关闭的资源。
不过,我们仍然可以使用 finally块来执行我们想要的任何其他类型的清理。
扩充
try-with-resources 语句中可以声明多个资源,语法是使用分号 ;
分隔各个资源:
public static void main(String[] args) throws IOException{
try (Scanner scanner = new Scanner(new File("players.txt"));
PrintWriter writer = new PrintWriter(new File("writers.txt"))) {
while (scanner.hasNext()) {
writer.print(scanner.nextLine());
}
}
}
同时存在多个异常
代码可以抛出多个异常,我们可以分别处理多个 catch 块:
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (IOException e) {
logger.warn("Player file wouldn't load!", e);
return 0;
} catch (NumberFormatException e) {
logger.warn("Player file was corrupted!", e);
return -1;
}
}
另请注意,我们没有捕获 FileNotFoundException,那是因为它继承自 IOException。当我们捕获 IOException 以后,Java 将认为它的任何子类也已处理。
但是,假设我们需要将 FileNotFoundException 与更一般的 IOException区别对待:
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile)) ) {
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException e) {
logger.warn("Player file not found!", e);
return 0;
} catch (IOException e) {
logger.warn("Player file wouldn't load!", e);
return 0;
} catch (NumberFormatException e) {
logger.warn("Player file was corrupted!", e);
return 0;
}
}
Java 允许我们分别处理子类异常,记住将子类异常放在捕获列表中的较前位置。
联合 catch
Java 7 以后,当我们知道处理错误的方式相同时,可以在同一块中捕获多个异常,语法是使用分号 |
分隔各个异常:
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (IOException | NumberFormatException e) {
logger.warn("Failed to load score!", e);
return 0;
}
}
异常抛出 throw
如果我们不想自己处理异常,或者我们想生成我们的异常让别人处理,那么我们需要熟悉 throw
关键字。
假设我们有以下我们自己创建的检查异常:
public class TimeoutException extends Exception {
public TimeoutException(String message) {
super(message);
}
}
抛出一个检查异常
就像从一个方法返回一样,我们可以在任何时候抛出 。
当然,当我们试图表明出现问题时,我们应该抛出:
public List<Player> loadAllPlayers(String playersFile) throws TimeoutException {
if ( time > 10 ) {
throw new TimeoutException("This operation took too long");
}
}
因为 throw 了 TimeoutException,我们还必须在签名中使用 throws
关键字,以便我们方法的调用者知道要处理它。
抛出未经检查的异常
如果我们想做一些事情,比如验证输入,我们可以使用未经检查的异常:
public List<Player> loadAllPlayers(String playersFile) {
if(playersFile == null) {
throw new IllegalArgumentException("Filename can't be empty");
}
// ...
}
因为 IllegalArgumentException
是运行时异常,所以我们不必标记该方法。
包装并重新抛出
我们也可以选择重新抛出我们捕获的异常:
public List<Player> loadAllPlayers(String playersFile)
throws PlayerLoadException {
try {
// ...
} catch (IOException io) {
throw new PlayerLoadException(io);
}
}
这种写法在实际开发中十分常见,我们需要将系统定义的异常转换为自己定义的异常,从而对异常统一处理。
异常继承
当我们用throws 关键字标记方法时,子类抛出的异常只能与父类一致,或者比父类更小。
finally 中 return
在 finally 中使用 return ,JVM 将丢弃异常,即使它是由我们的代码抛出的:
public int getPlayerScore(String playerFile) {
int score = 0;
try {
throw new IOException();
} finally {
return score; // <== IOException 无法抛出
}
}
更糟糕的是,可能出现下面的情况:
public int getPlayerScore(String playerFile) {
int score = 0;
try {
return 2;
} finally {
return -1; // <== the IOException is dropped
}
}
总结
异常是 Java 中非常重要的一个机制,有些人会觉得这个 Java 的异常处理臃肿,某些场合下,可以异常的代码量会超过真实业务。有些人会赞叹 Java 异常处理的强大,正是我们在异常上的仔细入微,才使得应用程序能长时间的稳定运行。