Java异常处理的9个最佳实践
无论你是新手还是资深程序员,复习下异常处理的实践总是一件好事,因为这能确保你与你的团队在遇到问题时能够处理得了它。
在 Java 中处理异常并不是一件易事。新手觉得处理异常难以理解,甚至是资深开发者也会花上好几个小时来讨论是应该抛出抛异常还是处理异常。
这就是为何大多数开发团队都拥有一套自己的异常处理规范。如果你初进团队,你也许会发现这些规范和你曾使用的规范大相径庭。
尽管如此,这里还是有一些被大多数团队所遵循的最佳实践准则。以下9个最重要的实践方法能帮助你开始进行异常处理,或提高你的异常处理水平。
1、在 Finally 中清理资源或使用 Try-With-Resource 语句
在实际开发中会经常遇到在 try 中使用资源的情况,比如一个InputStream,在使用后你需要关闭它。在这种情况下,一个常见的错误是在 try 的尾部关闭了资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public void doNotCloseResourceInTry() { FileInputStream inputStream = null ; try { File file = new File( "./tmp.txt" ); inputStream = new FileInputStream(file); // use the inputStream to read a file // do NOT do this inputStream.close(); } catch (FileNotFoundException e) { log.error(e); } catch (IOException e) { log.error(e); } } |
这种情况的问题是,只要异常没被抛出,程序就能很好地运行。所有在 try 中的代码都将被正常执行,资源也会被关闭。
但是,用 try 总是有原因的。当你调用一个或多个可能会抛出异常的方法或自己主动抛出异常时,程序可能会无法到达 try 的尾部。于是在最后,资源将不被关闭。
因为,你应该将所有清理资源的代码放进 finally 中,或使用 try-with-resource 语句。
使用 Finally
与 try 相比,无论是 try 中的代码被成功执行,还是在 catch 中处理了一个异常后,Finally 中的代码总会被执行。因此,你可以确保所有已打开的资源都将被关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public void closeResourceInFinally() { FileInputStream inputStream = null ; try { File file = new File( "./tmp.txt" ); inputStream = new FileInputStream(file); // use the inputStream to read a file } catch (FileNotFoundException e) { log.error(e); } finally { if (inputStream != null ) { try { inputStream.close(); } catch (IOException e) { log.error(e); } } } } |
Java 7 的 Try-With-Resource 语句
你还可以选择 try-with-resource 语句,在我的这篇Java 异常处理入门中有更为详细的介绍。
如果你在资源中实现了AutoCloseable接口的话,就可以使用 try-with-resource 语句了,这也是大多数 Java 标准资源的做法。如果你在 try-with-resource 中打开了一个资源,在 try 中的代码被执行或异常处理后,这个资源将会被自动关闭。
1
2
3
4
5
6
7
8
9
10
|
public void automaticallyCloseResource() { File file = new File( "./tmp.txt" ); try (FileInputStream inputStream = new FileInputStream(file);) { // use the inputStream to read a file } catch (FileNotFoundException e) { log.error(e); } catch (IOException e) { log.error(e); } } |
2、抛出更具体的异常
你抛出的异常越具体、越明确越好。时刻牢记这点,特别是如果有一位并不了解你代码的同事,或几个月后的你需要调用自己的方法并处理异常时。
因此,你需要确保提供尽可能多的信息,这会使得你的 API 更易于理解。这样,调用你方法的人可以更好地处理异常,从而避免额外的诸如此类的检查。
所以,应该找到与你的异常事件最符合的类,比如抛出一个NumberFormatException而不是 IllegalArgumentException(注:例如将参数转换为数值出错时,应该抛出具体的 NumberFormatException ,而不是笼统的 IllegalArgumentException )。请避免抛出一个不具体的异常。
1
2
3
4
5
6
|
public void doNotDoThis() throws Exception { ... } public void doThis() throws NumberFormatException { ... } |
3、为你的异常编写文档
当你在方法签名中指定一个异常时,你也应该在Javadoc 中记录它。
所以,请确保在 Javadoc 中增加 @throws 声明,并描述可能会导致异常的情况。
1
2
3
4
5
6
7
8
9
|
/** * This method does something extremely useful ... * * @param input * @throws MyBusinessException if ... happens */ public void doSomething(String input) throws MyBusinessException { ... } |
4、将描述信息与异常一同抛出
这个方法背后的思想和前两个是类似的。但这一次,你不必给你的方法调用者提供信息。对于任何遭遇异常错误并需要搞清楚错误原因的人来说,异常信息总是在异常出现的同时,被记录在了日志中,或打印在了屏幕上。
因此,请尽可能精确地描所以,最好不要在 catch 中使用 Throwable ,除非你能确保自己处于一些特定情况下,比如你自己足以处理错误,又或被要求处理错误时。述异常事件,并提供最相关的信息以令其他人能够理解发生了什么异常时。
别误会我的意思了。你没必要去写上一大段的文字,但你应该用一两句简短的话来解释一下异常发生的原因。这能让你的开发团队明白问题的严重性,也能让你更容易地分析服务事故。
如果你抛出了一个特定的异常,它的类名很可能就已经描述了这是什么类型的错误了。所以,你不需要提供很多额外的描述信息。一个很好的例子是,当你提供了一个错误格式的 String 类型参数时,java.lang.Long 构造函数就会抛出 NumberFormatException 。
1
2
3
4
5
|
try { new Long( "xyz" ); } catch (NumberFormatException e) { log.error(e); } |
NumberFormatException 的类名已经告诉了你问题的类型。所以异常信息只需要返回导致问题的输入字符串就行了。如果异常类的名字不能表明其含义,那么你还需要在异常信息中提供必要的解释信息。
1
|
17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz" |
5、优先捕获具体的异常
大多数 IDE 都能帮你做到这点。当你尝试优先捕获不那么具体的异常时, IDE 会报告给你这是一个不能到达的代码块。
这个问题的原因是只有第一个匹配到异常的 catch 块才会被执行。所以,如果你先 catch 了一个 IllegalArgumentException ,你将永远无法到达处理更具体异常 NumberFormatException 的 catch 块中,因为 NumberFormatException 是 IllegalArgumentException 的子类。
所以,请优先捕获更具体的异常,并把不那么具体的 catch 块放在后面。
在下面你可以看到这样的一个 try-catch 语句示例。第一个 catch 处理所有的 NumberFormatExceptions 异常,第二个 catch 处理 NumberFormatException 异常以外的 illegalargumentexception 异常。
1
2
3
4
5
6
7
8
9
|
public void catchMostSpecificExceptionFirst() { try { doSomething( "A message" ); } catch (NumberFormatException e) { log.error(e); } catch (IllegalArgumentException e) { log.error(e) } } |
6、不要捕获 Throwable
Throwable是所有 exceptions 和 errors 的父类。虽然你可以在 catch 子句中使用它,但你应该永远别这样做!
如果你在 catch 子句中使用了 Throwable ,它将不仅捕获所有异常,还会捕获所有错误。这些错误是由 JVM 抛出的,用来表明不打算由应用处理的严重错误。OutOfMemoryError和 StackOverflowError就是典型的例子,这两种情况都是由一些超出应用控制范围的情况导致的,无法处理。
所以,最好不要在 catch 中使用 Throwable ,除非你能确保自己处于一些特定情况下,比如你自己足以处理错误,又或被要求处理错误。
1
2
3
4
5
6
7
|
public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // don't do this! } } |
7、不要忽略异常
你分析过只有用例的第一部分代码被执行的 bug 报告吗?
这通常是由于忽略异常而导致的。开发者可能十分确定这个异常不会被抛出,然后添加了一个无法处理或无法记录这个异常的 catch 。当你找到这个 catch 时,你很可能会发现这么一句著名的注释: “This will never happen”。
1
2
3
4
5
6
7
|
public void doNotIgnoreExceptions() { try { // do something } catch (NumberFormatException e) { // this will never happen } } |
没错,你可能就是在分析一个永远也不会发生的问题。
所以,请你务必不要忽略异常。你不知道代码在将来会经历怎样的改动。有些人可能会误删异常事件的验证,而完全没意识到这会产出问题。或者抛出异常的代码被修改了,相同的类被抛出了多个异常,而调用它们的代码并不能阻止这些异常发生。
你至少应该把日志信息打印出来,告诉那些无意识下错误操作的人需要检查这里。
1
2
3
4
5
6
7
|
public void logAnException() { try { // do something } catch (NumberFormatException e) { log.error( "This should never happen: " + e); } } |
8、不要同时打印并抛出异常
这可能是本文中最常被忽略的一条实践准则了。你可以在许多代码片段甚至库中发现这个问题,异常被捕获,打印,再被重新抛出。
1
2
3
4
5
6
|
try { new Long( "xyz" ); } catch (NumberFormatException e) { log.error(e); throw e; } |
这样也许会很直观地看到被打印的异常,异常再被重新抛出,调用者也能很好地处理它。但这样会使多个错误信息被同个异常给打印出来。
1
2
3
4
5
6
7
|
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz" Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:589) at java.lang.Long.(Long.java:965) at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63) at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58) |
额外的信息并不能提供更多的错误细节。如第4条准则中所述,异常信息应该准确描述异常事件。 Stack Trace (堆栈追踪)会告诉你异常在哪个类、哪个方法、哪个行中被抛出。
如果你需要添加额外的信息,你应该将异常捕获并包装在自定义的的异常中,但要确保遵循下面的第9条实践准则。
1
2
3
4
5
6
7
|
public void wrapException(String input) throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException( "A message that describes the error." , e); } } |
所以,只有在你想要处理一个异常的时候才去捕获它。否则,在方法签名处指明这个异常让调用者关注就好了。
9、包装异常但不要丢弃原始异常
有时候将异常包装成一个自定义异常会比捕捉一个标准异常要更好。一个典型的例子是应用或框架的特定业务异常。这允许你添加额外的信息,也能为你的异常类实现一个特定的处理方法。
当你这么做的时候,一定要确保原始的异常设为 cause 。 Exception 类提供了一系列的特定构造方法,这些方法可以接受 Throwable 作为参数(注:如Exception(String message, Throwable cause)
)。否则,你将会丢失原始异常的 stack trace 与信息,这会使你分析导致异常的事件变得十分困难。
1
2
3
4
5
6
7
|
public void wrapException(String input) throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException( "A message that describes the error." , e); } } |
总结
如你所见,当决定该抛出还是捕获异常时候,你需要去考虑很多方面。以上的大多数实践准则都是为了提高你代码和 API 的可读性与可用性。
异常是不仅是一个错误处理机制,同时也是一个沟通媒介。因此,你应该与你的同事一起讨论哪些是你想要应用的最佳实践与准则,以便所有人都能理解相关的基本概念,并用同样的方式在实际中应用这些准则。
原文链接: dzone 翻译: ImportNew.com- Marticles
译文链接: http://www.importnew.com/29603.html
Java异常处理的9个最佳实践的更多相关文章
- 《转载》Java异常处理的10个最佳实践
本文转载自 ImportNew - 挖坑的张师傅 异常处理在编写健壮的 Java 应用中扮演着非常重要的角色.异常处理并不是功能性需求,它需要优雅地处理任何错误情况,比如资源不可用.非法的输入.nul ...
- Java异常处理的10个最佳实践
本文作者: ImportNew - 挖坑的张师傅 未经许可,禁止转载! 异常处理在编写健壮的 Java 应用中扮演着非常重要的角色.异常处理并不是功能性需求,它需要优雅地处理任何错误情况,比如资源不可 ...
- Java 异常处理的 9 个最佳实践
在 Java 中,异常处理是个很麻烦的事情.初学者觉得它很难理解,甚至是经验丰富的开发者也要花费很长时间决定异常是要处理掉和抛出. 所以很多开发团队约定一些原则处理异常.如果你是一个团队的新成员,你可 ...
- Java 异常处理的 20 个最佳实践,你知道几个?
异常处理是 Java 开发中的一个重要部分,是为了处理任何错误状况,比如资源不可访问,非法输入,空输入等等.Java 提供了几个异常处理特性,以try,catch 和 finally 关键字的形式内建 ...
- Java 编程中关于异常处理的 10 个最佳实践
异常处理是Java 开发中的一个重要部分.它是关乎每个应用的一个非功能性需求,是为了处理任何错误状况,比如资源不可访问,非法输入,空输入等等.Java提供了几个异常处理特性,以try,catch 和 ...
- Java 处理异常 9 个最佳实践,你知道几个?
1. 在Finally中清理资源或者使用Try-With-Resource语句 使用Finally Java 7的Try-With-Resource语句 2. 给出准确的异常处理信息 3. 记录你所指 ...
- 说出几点 Java 中使用 Collections 的最佳实践?
这是我在使用 Java 中 Collectionc 类的一些最佳实践: a)使用正确的集合类,例如,如果不需要同步列表,使用 ArrayList 而不是 Vector. b)优先使用并发集合,而不是对 ...
- Java 服务 Docker 容器化最佳实践
转载自:https://mp.weixin.qq.com/s/d2PFISYUy6X6ZAOGu0-Kig 1. 概述 当我们在容器中运行 Java 应用程序时,可能希望对其进行调整参数以充分利用资源 ...
- Java容器化参数配置最佳实践
Java是以VM为基础的,而云原生讲究的就是Native,天然的矛盾,虽然Quarkus是为GraalVM和HotSpot量身定制的K8s Native Java框架,生态原因切换成本太高,这种矛盾体 ...
随机推荐
- redis使用方法
redis缓存服务器笔记 redis是一个高性能的key-value存储系统,能够作为缓存框架和队列 但是由于他是一个内存内存系统,这些数据还是要存储到数据库中的 作为缓存框架: create/upd ...
- Migrations中的更新语句写法,摘要
public override void Up() { AlterColumn("dbo.Dispositions", "Property1", c => ...
- 非递归遍历二叉树Java版的实现代码(没写层次遍历)
直接上代码呵呵,里面有注解 package www.com.leetcode.specificProblem; import java.util.ArrayList; import java.util ...
- (转)运维老鸟教你安装centos6.5如何选择安装包
运维老鸟教你安装centos6.5如何选择安装包 原文:http://blog.51cto.com/oldboy/1564620 近来发现越来越多的运维小伙伴们都有最小化安装系统的洁癖,因此,找老男孩 ...
- Spark Mllib里的卡方检验
不多说,直接上干货! import org.apache.spark.mllib.stat.Statistics 具体,见 Spark Mllib机器学习实战的第4章 Mllib基本数据类型和Mlli ...
- WebGL之物体选择
原文地址: WebGL之物体选择 使用WebGL将图形绘制到画布后,如何与外部进行交互?这其中最关键的就是如何实现物体的选择.比如鼠标点击后判断是否选中了某个图形或图形的某个部分. 本节实现的效果: ...
- 工作经验(JNI篇)
我的工作是C++开发,主要是做底层的,由于要做跨平台的原因,常会做成JNI给Java调用,下面是工作时总结的经验希望有用 JNI只能使用C语言的方式编译,所以,要使用C++的话,要用 extern & ...
- 移动端toast 提示,HTML css 全屏垂直水平居中
- Garmin APP开发之入门
Garmin开发-入门 先附上几个已经开发完成的app日历 up down 翻月 start 回到当前月(就差农历了) 秒表和定时器一体app界面比较简单,但是实用,长按菜单键可以切换秒表和定时器,有 ...
- linux书籍推荐
<Linux/Unix设计思想> 图书将Unix与Linux的原理有效地结合起来,总结了Unix/Linux软件开发中的原则.在保留了第1版中Unix方面的内容的同时,强调了Linux和开 ...