捕获异常,而不是让程序崩溃
你已经知道,出问题时 Java 会抛出一个异常,而没有处理的异常会让程序停止运行,并打印堆栈跟踪。try-catch 代码块让你掌握主动权:把有风险的代码包进 try,一旦它抛出异常,Java 就会跳到匹配的 catch 代码块,而不是让程序崩溃。
10 / 0 抛出异常的那一刻,try 代码块剩下的部分就被跳过,控制流转到 catch。catch 执行完后,程序照常继续运行,不会终止。
catch 变量保存着异常
catch (ArithmeticException e) 中的 e 是一个真正的对象。它携带着出错的相关信息,其中最有用的是一条消息:
e.getMessage() 返回一段简短的描述。调试时,e.printStackTrace() 会打印完整的堆栈跟踪,准确显示异常是从哪里来的。光看消息找不到原因时,就用它。
捕获特定类型,而不是捕获一切
catch 代码块只会捕获与其声明类型(或子类)匹配的异常。新手最大的错误,是为了让问题“消失”而捕获 Exception:
// Don't do this - it hides real bugs
try {
doWork();
} catch (Exception e) {
// swallows NullPointerException, typos, logic errors... everything
}
捕获你真正能处理的、范围最窄的类型。如果你预期会遇到错误的数字输入,就捕获 NumberFormatException。任何你没预料到的异常都应该让它继续传播,这样你才能真正发现它,而不是让程序在出错的状态下悄悄继续运行。
处理多种异常类型
你可以叠加多个 catch 代码块。Java 会从上往下检查,并执行第一个匹配的代码块,所以要按从最具体到最通用的顺序排列:
如果两种类型的处理方式相同,就用 | 写一个多重捕获,不必把代码块重复一遍:
有一个坑:更通用的类型必须放在具体类型之后。如果把 catch (Exception e) 放在最前面,后面更具体的代码块就永远执行不到,编译器会直接报错。
finally 总会执行
finally 代码块会在 try 和所有 catch 之后执行,无论发生了什么:成功、捕获了异常,甚至是提前 return。必须执行的清理工作就应该放在这里。
无论异常是否触发,都会打印“Closing resource”。不过要避免在 finally 里 return,因为它可能悄悄丢掉一个异常,或者覆盖 try 代码块返回的值。
try-with-resources 帮你自动关闭
当你使用必须关闭的东西时,比如文件、网络连接或数据库语句,在 try (...) 里声明它,代码块结束时就会自动关闭,即使抛出了异常也一样。任何实现了 AutoCloseable 接口的类型都可以这样用。
它取代了在 try 中打开、在 finally 中关闭的旧写法,而且更不容易出错,因为你不可能忘记关闭。只要处理的是可关闭的资源,就优先使用它。
不要用异常控制正常流程
try-catch 用于异常情况,而不是普通的条件判断。捕获异常比一次简单的检查开销更大,也会让代码更难读。如果你可以先用 if-else 判断来检查条件,就这样做:
// Avoid: using a catch to test for a missing key
try {
process(map.get(key).trim());
} catch (NullPointerException e) {
// handle missing key
}
// Prefer: check explicitly
String value = map.get(key);
if (value != null) {
process(value.trim());
}
把 catch 留给真正超出你控制范围的情况:错误的用户输入、缺失的文件、网络故障。
下一页:NullPointerException
在 Java 中,你最常捕获(也最常引发)的异常就是 NullPointerException:只要你在一个结果为 null 的东西上调用方法,它就会立刻出现。下一页我们将深入了解它究竟是怎么触发的、如何阅读它的堆栈跟踪,以及从一开始就避免它的好习惯。
常见问题
Java 中如何使用 try-catch?
把可能抛出异常的代码放进 try 代码块,再加一个 catch 代码块,写明你要处理的异常类型:try { risky(); } catch (IOException e) { ... }。如果 try 中的代码抛出了匹配的异常,Java 会直接跳到 catch 代码块,而不是让程序崩溃。变量(e)保存着异常对象,你可以用 e.getMessage() 读取它的消息。
Java 中的 finally 代码块有什么用?
无论发生什么,finally 代码块都会在 try/catch 之后执行:不管代码成功、抛出异常,还是提前 return 了。它适合放必须执行的清理工作,比如关闭文件或释放锁。如果是关闭资源,通常用 try-with-resources 更简洁,因为它会自动关闭资源。
应该捕获 Exception 还是特定的异常类型?
捕获你真正能处理的、最具体的类型。捕获 Exception(更糟的是 Throwable)会吞掉所有问题,包括 NullPointerException 这样的 bug,并掩盖真正的原因。如果你预期的是 NumberFormatException,就捕获它,让意料之外的异常继续向上传播,这样你才能发现它们。