在 Java 中处理异常并不是一个简单的事情。需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。
抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。
异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。
异常处理 - 阿里巴巴Java开发手册 (oschina.io)
只针对不正常的情况才使用异常
异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。 Java 类库中定义的可以通过预检查方式规避的 RuntimeException
异常不应该通过 catch
的方式来处理,比如:NullPointerException
,IndexOutOfBoundsException
等等。
比如,在解析字符串形式的数字时,可能存在数字格式错误,不应该通过 catch Exception
来实现
-
异常机制的设计初衷是用于不正常的情况,所以很少会有JVM实现试图对它们的性能进行优化。所以,创建、抛出和捕获异常的开销是很昂贵的。
-
把代码放在
try-catch
中返回阻止了 JVM 实现本来可能要执行的某些特定的优化。 -
对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的 JVM 实现会将它们优化掉。
在 finally 块中清理资源或者使用 try-with-resource 语句
使用类似 InputStream
这种需要使用后关闭的资源时,需要在 finally
语句块中手动 .close
掉资源;或是在实现了 AutoClosable/Closable
接口的资源中使用 try-with-source
语法自动关闭资源。
而不是将 close
方法写在 try
语句块中,若其中发生了异常,异常之后的语句就不会执行,可能导致资源没有关闭。
尽量使用标准的异常
代码重用是值得提倡的,这是一条通用规则,异常也不例外。
在编写代码时,可以 利用现有的异常类来匹配某些特定的错误情况,而不是每次都创建新的自定义异常类
重用现有的异常有几个好处:
-
通过重用现有的异常类,可以减少不必要的代码量,使代码更加简洁清晰。
-
对于用到这些 API 的程序而言,使用现有的异常类可以让代码在出现相似错误时具有统一的异常处理逻辑,提高代码的可维护性和可读性。
-
异常类越少,意味着内存占用越小,并且转载这些类的时间开销也越小。
-
使用已有异常类可以传达更准确的错误信息,以及与特定错误类型相关的上下文信息。
Java标准异常中有几个是经常被使用的异常。如下表格:
异常 | 使用场合 |
---|---|
IllegalArgumentException | 参数的值不合适 |
IllegalStateException | 参数的状态不合适 |
NullPointerException | 在null被禁止的情况下参数值为null |
IndexOutOfBoundsException | 下标越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,对象检测到并发修改 |
UnsupportedOperationException | 对象不支持客户请求的方法 |
虽然它们是 Java 平台库迄今为止最常被重用的异常,但是,在许可的条件下,其它的异常也可以被重用。
例如,如果要实现诸如复数或者矩阵之类的算术对象,那么重用ArithmeticException
和 NumberFormatException
将是非常合适的。
如果一个异常满足需要,可以直接使用,但一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是名字的基础上。
选择重用哪一种异常并没有必须遵循的规则。
例如,考虑纸牌对象的情形,假设有一个用于发牌操作的方法,它的参数(handSize)是发一手牌的纸牌张数。
假设调用者在这个参数中传递的值大于整副牌的剩余张数。那么这种情形既可以被解释为
IllegalArgumentException
(handSize的值太大),也可以被解释为IllegalStateException
(相对客户的请求而言,纸牌对象的纸牌太少)。
也可以通过继承已有异常类,可以利用已有异常的属性和方法,创建自定义异常类,并扩展自现有的异常类
对异常进行文档说明
当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。
在 添加 @throws 声明,并且描述抛出异常的场景:
/*Method description
@throws MyBusinessException - businuess exception
description
*/
public void doSomething(String input) throws MyBusinessException {
// ...
}
在抛出 MyBusinessException
异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。
优先捕获最具体的异常
大多数 IDE 都可以帮助我们实现这个最佳实践。当尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。
匹配异常的第一个 catch 块会被执行。例如:如果首先捕获 IllegalArgumentException
,则永远不会到达应该处理更具体的 NumberFormatException
的 catch 块,因为它是 IllegalArgumentException
的子类。
应该优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。 第一个 catch 块处理所有 NumberFormatException
异常,第二个处理所有非 NumberFormatException
异常的 IllegalArgumentException
异常:
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
不要捕获 Throwable 类
Throwable
是所有异常和错误的超类。可以在catch
子句中使用它,但是永远不应该这样做!
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。
典型的例子是 OutOfMemoryError
或者 StackOverflowError
。
两者都是由应用程序控制之外的情况引起的,无法处理。
不要忽略异常
很多时候,开发者很有自信不会抛出异常,因此写了一个
catch
块,但是没有做任何处理或者记录日志。
现实中经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。
合理的做法是至少要记录异常的信息:
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e);
}
不要记录并抛出异常
很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑:
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志:
要提供更加有用的信息,那么可以将异常包装为自定义异常。仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。
包装异常时不要抛弃原始的异常
捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。
这样做时,要确保将原始异常设置为原因(使用 链式异常 中的 initCause
(Throwable Cause)方法)否则,将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。
不要使用异常控制程序的流程
异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低
例如,本应该使用if语句进行条件判断的情况下,却使用异常处理,这会严重影响应用的性能。
不要在finally块中使用return。
try
块中的 return
语句执行成功后,并不马上返回,而是继续执行 finally
块中的语句,如果此处存在 return
语句,则在此直接返回,会丢弃掉 try
块中的返回点。