在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花
费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都
会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。本文给出几个
被很多团队使用的异常处理 佳实践。
1. 在 finally 块中清理资源或者使用 try-with-resource 语句
当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在 try块的 后关闭资源。
问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资
源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而
且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的 后部分。结果就是,你
并没有关闭资源。
所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。
1.1 使用 finally 代码块
与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功
执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资
源。
1 public void doNotCloseResourceInTry() {
2 FileInputStream inputStream = null;
3 try {
4 File file = new File("./tmp.txt");
5 inputStream = new FileInputStream(file);
6 // use the inputStream to read a file
7 // do NOT do this
8 inputStream.close();
9 } catch (FileNotFoundException e) {
10 log.error(e);
11 } catch (IOException e) {
12 log.error(e);
13 }
14 }
1 public void closeResourceInFinally() {
2 FileInputStream inputStream = null;
3 try {
4 File file = new File("./tmp.txt");
5 inputStream = new FileInputStream(file);
6 // use the inputStream to read a file
7 } catch (FileNotFoundException e) {
8 log.error(e);
9 } finally {
10 if (inputStream != null) {
11 try {
12 inputStream.close();
13 } catch (IOException e) {
14 log.error(e);
15 }
16 }
17 }
18 }
1.2 Java 7 的 try-with-resource 语法
如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这
个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。
2. 优先明确的异常
你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异
常。
因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的
处理异常并且避免额外的检查。因此,总是尝试寻找 适合你的异常事件的类,例如,抛出一个
NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。
3. 对异常进行文档说明
当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可
以更好地避免或处理异常。
在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。
4. 使用描述性消息抛出异常
在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,
都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。
但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需
要用一到两句话描述即可。
1 public void automaticallyCloseResource() {
2 File file = new File("./tmp.txt");
3 try (FileInputStream inputStream = new FileInputStream(file);) {
4 // use the inputStream to read a file
5 } catch (FileNotFoundException e) {
6 log.error(e);
7 } catch (IOException e) {
8 log.error(e);
9 }
10 }
11
1 public void doNotDoThis() throws Exception {
2 ...
3 }
4 public void doThis() throws NumberFormatException {
5 ...
6 }
7
1 public void doSomething(String input) throws MyBusinessException {
2 ...
3 }如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信
息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被
java.lang.Long 类的构造函数抛出。
5. 优先捕获最具体的异常
大多数 IDE 都可以帮助你实现这个 佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问
的代码块。
但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获
IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch
块,因为它是 IllegalArgumentException 的子类。总是优先捕获 具体的异常类,并将不太具体的
catch 块添加到列表的末尾。你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个
catch 块处理所有 NumberFormatException 异常,第二个处理所有非
NumberFormatException 异常的IllegalArgumentException 异常。
6. 不要捕获 Throwable 类
Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你
永远不应该这样做!
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,
指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError
。两者都是由应用程序控制之外的情况引起的,无法处理。
所以, 好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。
1 try {
2 new Long("xyz");
3 } catch (NumberFormatException e) {
4 log.error(e);
5 }
1 public void catchMostSpecificExceptionFirst() {
2 try {
3 doSomething("A message");
4 } catch (NumberFormatException e) {
5 log.error(e);
6 } catch (IllegalArgumentException e) {
7 log.error(e)
8 }
9 }
1 public void doNotCatchThrowable() {
2 try {
3 // do something
4 } catch (Throwable t) {
5 // don't do this!
6 }
7 }
7. 不要忽略异常
很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。
但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出
的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录
异常的信息。
8. 不要记录并抛出异常
这可能是本文中 常被忽略的 佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次
抛出的逻辑。如下:
这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下:
如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装
为自定义异常。
1 public void doNotIgnoreExceptions() {
2 try {
3 // do something
4 } catch (NumberFormatException e) {
5 // this will never happen
6 }
7 }
1 public void logAnException() {
2 try {
3 // do something
4 } catch (NumberFormatException e) {
5 log.error("This should never happen: " + e);
6 }
7 }
1 try {
2 new Long("xyz");
3 } catch (NumberFormatException e) {
4 log.error(e);
5 throw e;
6 }
1 17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatExcep tion:
For input string: "xyz"
2 Exception in thread "main" java.lang.NumberFormatException: For input str ing:
"xyz"
3 at java.lang.NumberFormatException.forInputString(NumberFormatException.j
ava:65)
4 at java.lang.Long.parseLong(Long.java:589)
5 at java.lang.Long.(Long.java:965)
6 at com.stackify.example.TestExceptionHandling.logAndThrowException(TestEx
ceptionHandling.java:63)
7 at
com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:5
8)因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。
9. 包装异常时不要抛弃原始的异常
捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针
对的异常处理。
在你这样做时,请确保将原始异常设置为原因(注:参考下方代码
NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的
构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将
会使分析导致异常的异常事件变得困难。
10. 不要使用异常控制程序的流程
不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处
理,这是非常不好的习惯,会严重影响应用的性能。
11. 使用标准异常
如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异
常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创
建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。
12. 异常会影响性能
异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一
个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。
仅在异常情况下使用异常;
在可恢复的异常情况下使用异常;尽管使用异常有利于 Java 开发,但是在应用中 好不要捕获太多
的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰
到好处的信息。
public void wrapException(String input) throws MyBusinessException {
3 // do something
4 } catch (NumberFormatException e) {
5 throw new MyBusinessException("A message that describes the error.", e);
6 }
7 }
1 public void wrapException(String input) throws MyBusinessException {
2 try {
3 // do something
4 } catch (NumberFormatException e) {
5 throw new MyBusinessException("A message that describes the error.", e);
6 }
7 }
813. 总结
综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代
码的可读性或者 API 的可用性。
异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要
制定出一个 佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。