【IT168 评论】处理 Java 中的异常情况并不是个轻松的话题,什么样的异常需要如何处理,常让新手们觉得难以理解,甚至有经验的开发人员也可能花几个小时讨论不决。
正因如此,大多数开发团队有着一套自己的规则,如果你是团队里的新人,你会对团队之间对于这个规则的巨大差别感到惊讶。
即便是这样,也有一些大多数团队都使用的好做法。下面列出了 9 个比较重要的处理方式,可以帮你上手或者提升你的异常处理能力。
1、清理 Finally 块中的资源,或使用 Try-With-Resource 语句
在 try 块中使用资源时常发生,比如 InputStream ,是需要在用完之后关掉的。一个通常发生的错误是,在 try 块的末尾关掉资源:
顺利的话,只要没有异常抛出,这种方法似乎可以工作得很好,try 中的所有语句都会被执行,然后资源也被关掉。
不过你因为某个原因添加了 try ,其中调用的一个或多个方法可能就会引发异常,甚至有可能是你自己引发了异常,此时就无法运行到 try 的结尾,结果资源没法被关掉。
因此,你应该把清理资源的代码都放在 finally 块中,或者使用 Try-With-Resource 语句。
使用 finally 块
和 try 中的最后几行不一样,无论是 try 块被成功执行后还是在 catch 块中处理了异常,finally 块总是能被执行。这样,你可以确保清掉所有打开的资源。
Java 7 的 Try-With-Resource 语句
另一个办法是使用 Try-With-Resource 语句。
如果你使用的资源实现接口是 AutoCloseable,就可以用这个语句,多数Java 标准资源都这么做。当你在 try 中打开资源,它可以在 try 执行完毕后或异常处理完后自动关闭资源。
2、更喜欢特定的异常
你引发的异常越具体越好。时刻想着那些并不知道你代码的同伴,或者几个月之后的你,他们需要调用你的方法处理异常。
所以,要尽可能地提供更多信息,确保你的 API 更容易理解。 这样,调用你的方法的人才能更好地处理异常,或者避免在检查上浪费多余的时间。
要想办法找到那个最合适你期望事件的类,比如引发一个 NumberFormatException 比IllegalArguementException 要好,请避免引发一个不明确的异常。
3、把指定的异常记录下来
无论在什么时候你在方法签名中指定了一个异常,你都应该在你的 Javadoc 中记录下来。这和上一个做法目的相同,都是给调用者尽量多的信息,便于他们避免或者处理异常。
所以,要确保你在 Javadoc 中添加了 @throws 申明,说明什么样的情况会引发异常。
4、用描述性消息引发异常
这条和前两条背后的想法差不多,只是这次不用给方法调用者提供什么信息了。因为日志文件中或者监视器里收到异常报告时,每个人都必须读取异常消息。
因此,应该尽可能准确地描述问题,并提供最相关的信息帮助了解异常事件。
请不要误解我,我并不是让你写一大段文字。你应该在一两句话中为该异常作出解释,这能帮助你的运维团队了解问题的严重程度,同时也让你在分析服务异常时更轻松。
如果你引发了一个特定的异常,它的类名就可能已经描述了错误类型,所以你也不用再提供更多信息了。NumberFormatException 是个比较好的例子,当你在给一个字符串提供了错的格式,类 java.lang.Long 的构造函数就会引发这个异常。
NumberFormatException,这个名字已经告诉你问题所在了,你只需要提供导致问题的字符串。如果异常类的名称不具有表达性,需要在消息中提供所需的信息。
5、优先抓住最具体的异常
多数 IDE 可以帮助你实现这一条。 当你在尝试优先捕获较少特定的异常时,它们会报告一个无法访问的代码块。
问题在于,只有与异常匹配的第一个 catch 块会被执行,如果你先抓住了 IllegalArgumentException,你就无法到达应该处理更具体的 NumberFormatException,因为NumberFormatException 是其子类。
总是优先捕获最具体的异常类,并将低精确度的 catch 块添加到列表末尾。
在以下代码段中,你可以看到一个这样的 try-catch 语句的示例。第一 catch 块处理所有的NumberFormatException,第二个则是处理所有非 NumberFormatException 的IllegalArgumentException。
6、别去抓可抛出的对象
可抛出(throwable)是所有异常和错误的超类,虽然你可以在 catch 语句中使用,但是你永远都不应该用它。
如果你在 catch 语句中使用可抛出对象,结果不仅仅是抓住所有的异常,它还会抓出所有的错误。错误由 JVM 抛出,会指出那些不打算被应用程序处理的严重问题。这一类型的代表例子是 OutOfMemoryError,以及 StackOverflowError,两者都是由不在应用控制之内的情况引发的,无法处理。
所以,除非你绝对确定处于异常情况下,并且你有能力或被要求去处理错误,否则不要去抓可抛出对象。
7、不要忽视异常
你有没有分析过一个只有用例的第一部分被执行了的错误报告?
这种情况的原因往往是一个被忽视的异常,那个开发者可能非常确定它不会再被抛出,还添加了一个并不能处理或者记录异常的 catch 块。然后,当你发现这个块时,你还很有可能找到一个名为“永远不会发生”的著名评论。
你当然有可能在分析一个会发生不可能的问题。
所以,不要忽视一个异常。你不知道代码将来会发生怎样的改变,可能有人会删除你用来防止异常事件的验证,全然不知这会导致问题。或者,抛出异常的代码被修改,导致在同一个类里抛出多个异常,而且调用代码也不能阻止所有的异常了。
你至少应该写一个日志消息,告诉大家发生了难以想象的事情,需要有人做检查。
8、别记录完又抛出
这一条可能是这个列表中最常被忽视的。不过,你总是能找到很多代码段甚至是库里,有异常被捕获、记录然后又抛出。
一般大家的直觉是,异常发生时记录它然后再把它抛出,这样可以方便调用者妥善处理该异常。可问题是往往大家会为同一个异常写入多个错误消息。
而且后添加的消息没有任何其他信息。我们在 第 4 条里说过,异常消息应该描述异常事件。然后堆栈跟踪会告诉你抛出异常的是哪个类,那种方法和哪一行。
如果你需要添加更多的信息,应该捕获异常并把它打包在一个自定义的信息中。 但请务必遵循第 9 条。
所以,只有在你想处理该异常时再捕获它。否则就在方法签名中指明它,让调用者去处理。
9、在不损耗异常的情况下打包它
有时,抓住标准的异常并将其打包到自定义异常中更好。比较典型的是应用程序或框架特定的业务异常,你可以添加其他信息,还可以为异常类执行特殊处理。
这样做的过程中,要确保把原始的异常设置为原因(异常类提供了一个特定的构造函数用来接受可抛出对象作为参数)。不这样做的话,原始异常的堆栈跟踪和消息就会丢失,导致异常的事件难以分析。
总结
如你所见,在你抛出或捕获异常的时候,应该考虑很多不同的事情,其中大多数都是为了提高代码的可读性或 API 的可用性。
异常通常是由一个错误的处理机制和同时使用的沟通媒介导致的。因此,你应该和同事讨论你要用的做法和规则,让每个人都理解通用的概念,然后用相同的方式执行这些做法和规则。