其实工作这么久了一直都没搞清楚到底如何来处理异常,偶然看到一篇外文感觉还不错,便把它翻译了下来,原文链接位于本文末尾处。

在java中处理异常并不是一件简单的事,不止初学者觉得它难以理解甚至连有经验的开发者也会花费几个小时来讨论某个异常应该抛出还是处理掉。

这就是为何大多数开发团队都拥有自己的规范来指明如何使用它们,如果你刚来到一个新的团队,你可能会发现新团队的准则与你之前遵循的大有不同。

尽管如此,这里还是有几条最佳准则被大多数团队所遵循。这里有9条准则可以帮助你提高处理异常的水平。

1、在Finally块中清理资源或者使用Try-With-Resource语句

在try块中使用资源的情况在开发中会经常碰到,比如一个InputStream,使用它之后你需要将它关闭。在这种情况下经常会看到在try块中去关闭资源的错误。

 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块不同的是——finally块中的语句总是会被执行,无论是try块中的语句成功执行还是你在catch块中处理了一个异常。因此所有开启的资源都能够确保被关闭。

 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,与之相关的详情在我的另一边文章——introduction to Java exception handling中有介绍。

如果你的资源实现了AutoCloseable接口的话你便可以使用try-with-resource语句,这也是大多数Java标准资源的做法,如果你在try-with-resource语句中声明了一个资源,它将会在try块中的语句执行后或者将异常处理后自动关闭。

 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(译者注:这句话缺少上下文,不明白作者的意思)。且避免抛出不明确的异常。

 public void doNotDoThis() throws Exception { ... }

 public void doThis() throws NumberFormatException { ... }

3、在文档里记录你的异常

每当你在方法签名处指定一个异常,都应该同时将它记录到Javadoc里边。

这与前一条准则的目的是一样的:给方法调用者提供尽可能多的信息,让他可以避免触发异常或者方便的他处理异常。

所以确保要在Javadoc里边添加@throws声明并且描述什么样的情况会导致异常。

 /**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException if ... happens
*/
public void doSomething(String input) throws MyBusinessException { ... }

4、将异常与它的描述信息一并抛出

这条准则的想法与前两条是相同的,但这次你不是给你的方法调用者提供信息,当这个异常信息被打印到日志文件或者反馈到你的监视工具时,它要能被每个想要了解发生了什么的人理解。

因此,我们应该尽可能精确的描述问题并且提供更接地气的消息来让他人理解发生了什么异常。

别误会我的意思,你没必因此写上一大段话,但你应该用一两句话简明扼要的解释一下异常的原因。以帮助你的运营团队了解发生了什么问题,同时这也会使你更容易分析问题原因。

如果你抛出一个明确的异常,它的类名很可能已经描述了这是怎样一个错误了,所以你不需要提供大量格外的信息,NumberFormatException就是一个很好的例子,当你给java.lang.Long的构造函数提供一个错误格式的String类型参数时便会抛出NumberFormatException。

 try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}

NumberFormatException的类名已经告诉你这是什么类型的问题了,它的异常消息只需要指明导致这个问题的输入字符串,如果异常类的名称不能达其意,你需要在异常消息中提供必要的信息。

 17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"

5、优先捕获更明确的异常

大部分IDE都会帮你遵循这条准则,当你把没那么明确的异常放在前面的时候他们会提示存在无法到达的代码块。

这是因为只有第一个匹配到的catch块才会被执行,所以如果你先捕获IllegalFormatException,你将永远无法到达处理NumberFormatException的catch块,因为NumberFormatException是IllegalArgumentException的子类。

所以应优先捕获明确的异常,将没那么明确的catch块放在后边。

如下代码片段中的try-catch语句。第一个catch块处理所有NumberFormatException,第二个则处理所有非NumberFormatException的IllegalArgumentException。

 public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}

6、不要捕获Throwable

Throwable是所有异常(Exception)和错误(Error)的父类,虽然它能在catch从句中使用,但永远都不要这样做!

如果你在catch从句中使用了Throwable,它将不仅捕获所有异常,它还将捕获所有错误,错误是由JVM抛出的,用来表明不打算让应用来处理的严重错误。

OutOfMemoryError和StackOverflowError便是典型的例子,它们都是由于一些超出应用处理范围的情况导致的。

 public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}

7、别忽略异常

你是否曾经分析过一份不完整的bug报告?

这通常是由于忽略异常导致的,这开发者大概很确定这里永远都不会抛出异常并加了一个不处理且不打印日志的catch块,当你找到这个块时,甚至很可能发现这么一句著名的注释——“This will never happen”

 public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}

好吧,你可能正在分析一个不可能发生的问题。

所以,请不要忽略异常,你不知道代码在将来会如何变动,可能有人会将防止该异常事件的校验移除掉且没有意识到这会产生问题,或者抛出异常的这段代码改变了,相同的类现在变成抛出多个异常,而调用它的代码没有预防所有的异常。

你至少要将日志打印出来告诉别人这里发生了异常,方便别人来检查。

 public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e);
}
}

8、不要打印异常日志的同时将其抛出

这可能是本文当中最常被忽视一条准则,你会在很多代码片段中甚至库中发现一个异常被捕获打印日志后被重新抛出。

 try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}

这样做可能确实是直观的看到了异常日志,然后将异常重新抛出,所以调用者也能正确的处理异常,但这样做会使一个异常打印多个异常信息。

 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条准则,异常消息应该描述异常事件,堆栈信息告诉你异常抛出的所在类、方法、行数。

如果你需要添加额外的信息,你应该将异常捕获并将其包在你的自定义异常中,但要确保遵循第9条准则。

 public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}

所以只有在你想要处理某个异常的时候才应该去捕获它,否则在方法签名处声明抛出该异常让调用者去关注它就好了。

9、包裹某个异常的同时不要丢弃它原本的信息

有时候我们需要捕获一个标准异常并用自定义异常包裹住它,一个典型的例子便是比如某个应用或者框架的指明业务异常,它允许你添加额外的信息,你也可以实现特别的异常处理方法。

当你这么做的时候,要确保将原本的异常作为原因设置到自定义异常里面,Exception类提供指定的构造方法可以接收Throwable类的对象作为参数,否则你将会丢失堆栈信息和原异常的消息,这将会令异常分析变得什么的困难。

 public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}

总结

如你所见,当你捕获或者抛出异常的时候你需要考虑很多事情,总的来说这些准则都是为了提高代码的可读性,API的可用性。

异常往往既是是错误处理机制也是沟通媒介,因此,你应该与你的同事一起讨论这些准则,使每个人都理解这些常规的概念,并以相同的风格在实践中应用它们。

原文链接:https://stackify.com/best-practices-exceptions-java/

译者博客:http://www.cnblogs.com/kcher90/

Java - 9个处理异常的最佳准则的更多相关文章

  1. Java中的受检异常

    Java中的受检异常 Java提供了三种异常类型,受检异常(checked exception).运行时异常(runtime exception).错误(error).那么这受检异常在实际开发中又有什 ...

  2. 【转】Java中关于异常处理的十个最佳实践

    原文地址:http://www.searchsoa.com.cn/showcontent_71960.htm 导读:异常处理是书写强健Java应用的一个重要部分,Java许你创建新的异常,并通过使用 ...

  3. 编写高质量代码:改善Java程序的151个建议(第一章:JAVA开发中通用的方法和准则)

    编写高质量代码:改善Java程序的151个建议(第一章:JAVA开发中通用的方法和准则) 目录 建议1: 不要在常量和变量中出现易混淆的字母 建议2: 莫让常量蜕变成变量 建议3: 三元操作符的类型务 ...

  4. java提高篇(十七)-----异常(二)

          承接上篇博文:java提高篇-----异常(一) 五.自定义异常 Java确实给我们提供了非常多的异常,但是异常体系是不可能预见所有的希望加以报告的错误,所以Java允许我们自定义异常来表 ...

  5. Java.lang.NoSuchFieldError: INSTANCE异常

    解决方案: java.lang.NoSuchFieldError: INSTANCE异常. 1.jar包重复了. 2.版本还不相同,如果包的版本不同也会报相应的错,不过一般情况自己导入的jar包主要看 ...

  6. attilax.java 注解的本质and 使用最佳实践(3)O7

    attilax.java 注解的本质and 使用最佳实践(3)O7 1. 定义pojo 1 2. 建立注解By eclipse tps 1 3. 注解参数的可支持数据类型: 2 4. 注解处理器 2 ...

  7. paip.java win程序迁移linux的最佳实践

    paip.java win程序迁移linux的最佳实践 1.class load路径的问题... windows哈第一的从calsses目录加载,,而linux优先从jar加载.. 特别的是修理了ja ...

  8. 在Servlet使用getServletContext()获取ServletContext对象出现java.lang.NullPointerException(空指针)异常的解决办法

    今天遇到了一个在servlet的service方法中获取ServletContext对象出现java.lang.NullPointerException(空指针)异常,代码如下: 1 //获取Serv ...

  9. java.sql.SQLException: Io 异常: Connection reset

    当数据库连接池中的连接被创建而长时间不使用的情况下,该连接会自动回收并失效,但客户端并不知道,在进行数据库操作时仍然使用的是无效的数据库连接,这样,就导致客户端程序报“ java.sql.SQLExc ...

随机推荐

  1. Python基础之常用模块(一)

    模块本质就是一个.py文件,在安装目录下的lib文件夹下可以看到 模块分为三个部分:内置模块(存在于解释器中),第三方模块(lib文件夹下),自定义模块(自己定义的) 1.time模块 import ...

  2. 史上最易懂——ReactNative分组列表SectionList使用详情及示例详解

    React Native系列 <逻辑性最强的React Native环境搭建与调试> <ReactNative开发工具有这一篇足矣> <解决React Native un ...

  3. CubieBoard开发板不用ttl线也不用hdmi线的安装方法

    本文重点在于CubieBoard开发板系统的初始化安装,并且不用ttl和hdmi线,开机就可以远程ssh进系统.本文适合没有配线的同学参考操作.事实上,无论有没有ttl线,按照本文的方法安装效率都是一 ...

  4. 将域名转移到 Google Domains

    之前存放域名用过 Godaddy.Dynadot.Namesilo 也有阿里云(万网)和腾讯云(新网),这回就用 Google Domains 啦! 话说 Google Domains 早已是 201 ...

  5. Node.js 入门简介

    Node.js简介 1.1 简介 V8引擎本身就是用于Chrome浏览器的JS解释部分,但是Ryan Dahl鬼才般的把这个V8搬到了服务器上,用于做服务器的软件. Node.js是一个专注于实现高性 ...

  6. String对象常用的一些方法

    anchor() 创建 HTML 锚. big() 用大号字体显示字符串. blink() 显示闪动字符串. bold() 使用粗体显示字符串. charAt() 返回在指定位置的字符. charCo ...

  7. 【NO.6】HTTP请求-配置-POST请求-GET请求

    直接举例来说 :) 例1:URL里面有”&”连接的情况 http://123.456.7.89:8080/aa/bb/cc/?userid=abcd@gmail.com&operati ...

  8. python进阶(7):面向对象进阶

    学了面向对象三大特性继承,多态,封装.今天我们看看面向对象的一些进阶内容,反射和一些类的内置函数. 一.isinstance和issubclass class Foo: pass class Son( ...

  9. 计蒜客模拟赛D2T1 蒜头君的兔子:矩阵快速幂

    题目链接:https://nanti.jisuanke.com/t/16442 题意: 有个人在第一年送了你一对1岁的兔子.这种兔子刚生下来的时候算0岁,当它在2~10岁的时候,每年都会生下一对兔子, ...

  10. 关于virtualenvwrapper的python, pip 的版本的问题

    关于virtualenvwrapper的python, pip 的版本的问题: 在创建虚拟环境时, 我们可以用 mkvirtualenv THE_NAME_OF_VENV --python=pytho ...