异常处理及思考
Java中应该如何处理异常,这个话题看似简单,不就是
try...catch
嘛,但是往往BUG更容易出现在一些简单地、我们更容易忽略的地方。一个好的异常处理能让开发人员快速定位异常信息和修复问题,同时也能更好的让使用人员进进行捕获和处理异常信息。
使用finally或try…with…resource关闭资源
如果我们在try代码块中需要使用到一些资源,比如InputStream
,在使用完之后我们需要将资源关闭。
<font color="red">错误示例如下:**</font>**
1 |
|
在上面这段代码中,只要在文件读取时没有出现异常,这段代码是可以正常工作的,但是只要在try块中的close()方法中抛出异常,资源就不会被关闭。
所以这种情况我们应该将资源关闭的代码放在finally中或者使用try…with…resource语句。
应该使用finally,在finally块中的代码不管是否出现异常,都会被执行,因此可以确保资源对象被关闭。
正确示例如下:
1 |
|
使用try…with…resource
随着资源使用的增多,当打开多个资源是对应的资源关闭也是一个问题。因为资源打开的越多,finally中嵌套的次数越多,这将导致大量的无用代码,从而导致代码的臃肿。
try…with…resource正是java 1.7中新增的语法糖功能,而通过这个语法糖功能,无需我们手动的关闭资源,程序会自动关闭我们打开的资源。
对应的条件:
- <font color="red">资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭**</font>**
- <font color="red">所有实现了 java.lang.AutoCloseable 接口(其中,它包括实现了 java.io.Closeable 的所有对象),可以使用作为资源**</font>**
- <font color="red">越晚声明的对象,会越早被close掉,即先开后闭原则**</font>**
示例如下:
1 |
|
使用更明确的异常
如果我们的方法需要向外抛出异常,那么异常类型越具体越好。因为在外部调用你代码的其他人对你内部的实现逻辑可能并不清楚,所以要确保能提供给他尽可能多的信息,可以让别人在使用你的方法时更容易理解,这样调用方可以更好地处理抛出的异常。
比如,在你的方法内容抛出NumberFormatException
比抛出IllegalArgumentException
或者直接抛出Exception
,所代表的含义就会更明确。
方法注释中对异常进行说明
如果你的方法声明了可能会抛出异常,那么在方法的文档注释中,应该对异常进行说明。这和上一条的目的一样,都是为了让方法的调用者能提前获得更多的信息,方便他避免在调用你方法时出现异常,或者更明确如果进行异常处理。
所以,我们应该在方法的文档注释中添加@throws声明,并说明什么情况下会抛出对应的异常。
示例如下:
1 |
|
在异常中携带足够的描述信息
这一点和前两条做法的目的类似。在异常中携带足够的描述信息,是为了在出现该异常时,能够在日志文件中查看异常信息时,能看到更有用的信息。
所以我们应该尽可能准确地描述出为什么抛出了这个异常,并提供最相关的数据信息让别人定位。
当然这里也不能太极端,你洋洋洒洒写一篇小作文,应该使用简短的一段信息描述,让运维同事能了解到这个问题的严重性,更轻松地分析问题所在。
也不用提供一堆额外的冗余信息,尽量做到足够精准。比如当你再创建一个Long对象时如果传入一个字符串,就会抛出NumberFormatException
。
NumberFormatException
的类名已经告诉我们出现的是数字格式化异常,所以在message
中只需要提供输入的字符串。如果你定义的异常类名不能很明确的表达出是什么异常,比如BusinessException
,你就应该在message
中表达出更多的信息。
示例如下:
1 |
|
控制台打印信息如下:
1 |
|
先捕获更明确的异常
一般在我们使用的IDE中,如果当你在做异常捕获时,先捕获了不太具体的异常比如Exception
,然后再捕获更具体的异常如IOException
,都会提示我们后面的catch块无法到达。所以我们应该先捕获最具体的异常类,将不太具体的异常类的捕获放在最后。
1 |
|
不要捕获Throwable
Throwable
是所有Exception
和Error
的父类。
虽然可以在catch
块中捕获它,但是我们不应该这样去做。因为如果使用了Throwable
,那么不仅会对所有抛出的Exception
进行捕获,还会捕获所有的Error
。
而当我们的程序抛出Error
时表示是一个无法处理的严重问题,例如典型的OutofMemoryError
,StackOverflowError
等,这两个Error
都是由程序无法控制并且不能处理的情况引起的。所以说,最好不要在你的catch
中捕获Throwable
,除非你非常确定try
块中的代码抛出的是可以处理的异常情况。
<font color="red">错误示例如下:**</font>**
1 |
|
不要将异常忽略
在你开发的时候可能非常确定不会抛出异常,并且在你开发时确实没有发生过抛出异常的情况,所以你在catch
块中没有对异常做任何处理。
1 |
|
但是,你其实不确定在将来会不会有人在你的try块中添加新的代码,并且他可能也不会意识到他添加的代码会导致有异常抛出,这将会导致在线上真的有异常产生,但是没有一个人知道。
所以,你至少应该在catch中打印一行日志,告诉同事,“警报,这里出现了一个不可能会出现的异常”。
1 |
|
不要打印日志后又将异常抛出
这一条可能绝大多数人都会犯过,我见过非常多别人的代码在异常处理时,先打印了一行异常日志,然后将异常抛出,或者转成一个RuntimeException
抛出。
甚至在一些开源框架中都有出现过。
1 |
|
你可能会认为这样做很直观,也没什么错,让调用你方法的人去处理就好了。但是这样一来,在日志中会对抛出的一个异常打印多条错误信息。
重复的日志并没有带来任何有价值的信息,参考上面第4条中描述,在异常信息中应该携带足够的信息,并且要做到精准。如果需要在添加其他信息,你应该将捕获到的异常封装在你的自定义异常中再进行抛出。
1 |
|
所以,我们应该只有在想对异常进行处理时捕获,否则就应该在抛出去,并且在方法前面上加以说明,让调用方去处理。
在包装异常时使用原始异常
通常在项目开发时,都会有一套自定义的异常,用于将API中的标准异常封装到自定义异常中,可以用于在外层做一些统一的异常处理。
但是我们在使用自定义异常对原始异常进行封装时,需要确保将原始异常作为cause保存在自定义异常中,否则你在外层将会丢失原始异常的堆栈跟踪信息,到你你无法通过异常信息分析抛出异常的具体原因。
1 |
|
总结
在抛出或者捕获异常时,我们应该考虑很多不同的事情,上面所说的大多数都是为了提高代码的可读性和提供给别人的API更易用。
通常异常不光是一种错误处理机制,同时还具备一定的信息媒介作用。我们应该遵循这些异常处理的规则和最佳实践,写出更规范,不让别人吐槽的好代码。