Java 中异常处理的 9 个最佳实践
在Java中进行处理异常并非是一件容易的事,初学者经常陷入困惑,甚至有经验的开发者也需要认真研讨哪些异常需要处理,哪些异常需要向上抛出。导致每个开发团队都会自己定制一套特有的异常处理规则,这使得新加入团队的成员都经历一段痛苦的适应期。
尽管如此,前辈们依然总结了几个最佳实践可以遵循,这些实践被绝大多数的团队所采用,本文将为你列出9个最常用且最重要的实践来帮助你提升异常处理的技能。
在做任何事的行动之前,知道为什么做?做了能解决什么问题?然后才去思考怎么做!这样不仅会让你思路更清晰,还可以让这件事更有价值。因此在进入探讨异常处理最佳实践的正题之前,我们首先需要解决两个问题:
什么是异常和异常处理?
为什么需要它们?
异常及异常处理
什么是异常?总结为一句话就是:程序在执行过程中产生的异常情况。当某些事情出现了错误异常就会发生,比如打开一个并不存在的文件、尝试在一个为null的对象上调用方法等等,都会发生异常。
异常是不可预知的,可是一旦它发生了就需要进行异常处理,可谓知错就改善莫大焉!异常处理是一种错误处理机制,如果你不对异常做任何处理,异常将会导致应用程序崩溃。
一旦你选择了进行处理异常,也就意味着你承认问题的发生,采用必要要的措施去让应用程序从错误中恢复,从而让业务继续进行,阻止应用程序崩溃。
实际上异常处理并不是处理问题的唯一一种方式,如今的高级语言一般都有异常处理机制,但比较古老的如C语言是通过返回错误码的方式来处理异常的。比如数组越界比较常用的返回值是-1。
这种方式的优点是代码逻辑易于推理,没有中断和代码跳转。另一方面,这种处理方式鼓励函数的调用者总是检查返回的错误码。但是这种检查容易造成代码污染,导致代码的可读性和可维护性降低。
错误代码的另一个严重的缺点是缺乏上下文信息,你可能知道错误码“-5”代表找不到文件,但究竟找不到哪个文件呢!错误码就无法表述了。
错误代码一般用于面向过程的语言,对面向对象的高级语言,有些场景是无能为力的,比如构造函数异常,是无法返回错误码的。
异常处理
当异常被抛出时,应用程序的流程就会被中断,如果没能及时处理异常,应用程序将崩溃。用户将看到异常信息,但那些信息大多他们是看不懂的,这将是一个很糟糕的用户体验,实际上异常信息还可以包装成非常友好的提示。
所以必须进行异常处理,哪怕是为了提高用户体验、记录问题日志、优雅地退出应用程序等。
我们可以使用代码块(try...catch...)来进行异常处理,当发生异常,通过try执行代码,如果发生异常,应用程序的流程将转移到catch中,catch捕捉到异常并进行必要的处理。
以上表述的异常处理原理对初学者依然比较抽象,我们来举个例子
分析下这个程序,在main中初始化有3个元素的数组,把这个数组传递给私有方法print4thItemInArray,在print4thItemInArray中试图获取数组的第4个元素,由于没有第4个元素将抛出“ArrayIndexOutOfBoundsException”异常,应用程序只会打印到“Third line”。
执行应用输出结果如下
First line
Second line
Third line
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at com.zqf.App.print4thItemInArray(App.java:20)
at com.zqf.App.main(App.java:14)
Process finished with exit code 1
现在我们修改下,增加异常处理
现在运行看看输出
First line
Second line
Third line
Have no four items!
Fourth line
Fith line
实际上这次的异常依然会发生,因为第4个元素的确不存在,所以在"Fourth element successfully displayed!"输出之前就抛出了异常,中断执行流程,但流程跳转到catch语句块了,catch只打印了一条“Have no four items”,继续向下执行。
Java异常体系
在Java中,所有的异常都有一个共同的祖先Throwable,它有2个子类:Exception(异常)和Error(错误),它们又各自有大量的子类。Exception(异常)和Error(错误)的共性和区别:两者都可以被捕捉,但前者可以被应用程序本身处理,后者是严重的,是无法恢复处理的。
最佳实践
1. 用Finally或Try-With-Resource清理资源
我们经常在try语句块使用资源,比如InputStream,使用完后需要关闭。经常犯的错误是在try语句块中关闭资源。如
这种方式看似非常完美,不会有异常抛出,所有的语句在try中执行,关闭IputStream释放资源。但试想一下:如果在“inputStream.close()”语句之前就抛出异常,会怎样呢?正常的流程会被中断并跳转,导致InputStream根本没关闭。
因此,应该把清理资源的代码放在finally或try-with-resource语句中。不管是正常执行完try语句块,还是异常处理完毕,都会执行finally语句块,而你需要确保在finally关闭所有打开的资源。
在JDK7引入了try-with-resource的语法,简单来说当一个资源对象(如InputSteam对象)实现了AutoCloseable接口,那么就可以在try关键字后的括号里创建实例,当try-catch语句块执行完毕后,会自动关闭资源,代码也会简洁许多。如下
2. 抛出具体异常
你抛出的异常越具体越好,不熟悉你代码的同事或者几个月之后的你,可能需要调用你这些方法并进行异常处理,所以尽可能多的提供信息,让你的API更容易理解,比如能用NumberFormatException就不要用 IllegalArgumentException,绝对避免直接使用不具体的Exception类。
3. 做好注释/文档
只要你在方法声明异常,就需要做好Javadoc的注释。这点和上一条最佳实践有相同的目标:提供给调用者尽可能多的信息,便于避免异常或进行异常处理。所以请确保你在Javadoc中添加了"@throws"声明,并且描述了造成异常的情况。
4. 异常携带可描述的信息
这条最佳实践和前面两条有点相似,但这条提供的信息不单是给方法调用者看的,而更多的是为了给记录日志或监控工具提供的,便于排查异常。实际上一般的异常类名就已经描述了问题的类型,你不必提供大量的附加信息,简洁凝练即可。比如NumberFormatException,当java.lang.Long构造函数抛出异常时会提供一句简短且清晰的文本来描述。
NumberFormatException 的名字就已经告诉你异常的种类,它携带的信息仅告诉你提供的字符串会导致异常,但如果异常名字不能表达异常种类,就需要提供更多的信息。上述的异常信息如下
17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"
如果你仔细看下JDK的源码,就会清楚java.lang.Long在构造器中做了各种校验,当某些校验失败会调用NumberFormatException.forInputString,而静态方法forInputString会把java.lang.Long的构造参数格式化后再构造一个新的NumberFormatException实例并抛出
5. 先捕捉子类异常
很多IDE都会帮助你进行最佳实践,如果你先捕捉父类异常再捕捉子类异常,它们会告诉你后面的代码不可到达或者警告已经被捕捉,因为是按照catch在在代码中顺序执行的。
所以如果先捕捉IllegalArgumentException,将不能捕捉到其子类NumberFormatException,因此最佳时间是总是先捕捉更多信息的异常(子类),再捕捉父类。如
6. 不要捕捉Throwable
从Java异常体系的图中可知,Throwable是所有异常(Exception)和错误(Error)的祖先,Throwable是可以被捕捉,但请不要捕捉。如果你捕捉了Throwable,那么不仅仅是捕捉了异常,还捕捉了错误。但错误是无法恢复,它是被JVM抛出的严重错误,应用程序对这类错误是无能为力的。
7. 不要忽略异常
你是否记得曾几何时,在分析bug时遇到代码只执行了前半部分,但却不知为何。有些开发者经常捕捉了异常,但凭经验认为异常决定不可能发生,导致没有做异常处理
实际上在大多数情况下它都发生了,因为随着时间和业务逻辑的变更,try代码块的内容变更了,导致了异常发生,而你的自信不仅害了你也害了后来人。建议catch中至少要留一条日志,来告知异常问题,方便排查。
8. 不要在仅仅记录日志后向上抛出异常
“不要在记录日志后向上抛出异常”,这是最佳实践中最容易被忽视的一条。你会发现在大量的代码片段,甚至类库中经常捕捉异常、记录日志,然后抛出异常。
直观的感觉是记录异常,然后抛出异常让调用者可以恰当的处理,但同一个异常多处日志记录,会让人迷惑,请参考第4条最佳实践:简洁凝练。
如果你真的需要给调用者提供更多信息,可以参考下一条最佳实践:包装异常。
9. 包装异常
比较可取的做法是捕捉到标准异常,根据实际业务自定义包装异常再向上抛出。在包装异常时通常把原始异常作为构造参数传进来,否则会丢失栈的跟踪信息,造成分析困难。
总结
如你所见,当你处理异常或抛出异常是有许多要考虑的事情,大多是从代码的可读性和API可用性来考虑。因此,最好和同事一起讨论异常处理的最佳实践,从而达成共识、步调一致,不仅提高工作效率,还能避免不可预知的异常。
最新、更多漫画请关注微信公众号:码农神说。转载开白可联系助手,微信号:codeceo-01
相关阅读
版权声明: 本文为 InfoQ 作者【码农神说】的原创文章。
原文链接:【http://xie.infoq.cn/article/e1acf36fa0655c321f673c230】。文章转载请联系作者。
评论