Java异常处理不简单,多个团队使用的最佳实践

译者:飒然Hang

Java异常处理不简单,多个团队使用的最佳实践

译文链接:

原文链接:

在 Java 中处理异常并非易事。初学者难以理解,有经验的开发者也需花费大量时间思考如何处理异常,比如要处理哪些异常以及怎样处理等。这是绝大多数开发团队会制定规则来规范异常处理的原因。并且团队间的这些规范往往差异很大。

本文给出几个被很多团队使用的异常处理最佳实践。

在 Finally 块里进行资源的清理工作或者运用 try-with-resource 语句。

使用类似 InputStream 这种需要使用后关闭的资源时,一个常见的错误情况是在 try 块的末尾去关闭资源。

公共的方法是不要在 try 语句块中关闭资源。
创建了一个名为 inputStream 的 FileInputStream 对象,并且初始时将其赋值为 null 。
try {
创建了一个名为 file 的文件对象,该文件的路径为 "./tmp.txt"。
创建了一个用于读取文件的输入流对象,该输入流对象是通过将指定的文件传递给 FileInputStream 的构造函数而创建的,即 inputStream = new FileInputStream(file)。
使用输入流来读取一个文件。
// do NOT do this
inputStream.close();
如果捕获到 FileNotFoundException 异常,那么就会执行以下操作。
log.error(e);
} catch (IOException e) {
log.error(e);
}
}

上述代码在没有任何异常的情况下运行是没有问题的。然而,当 try 块中的语句抛出异常,或者自行实现的代码抛出异常时,就不会执行最后的关闭语句,进而资源也无法被释放。

合理的做法是把所有清理的代码都放置在 finally 块里。也可以使用 try-with-resource 语句。

在 finally 块中关闭资源。首先判断资源是否为空,若不为空则尝试关闭它。若关闭过程中出现异常,记录异常信息但不抛出,以确保资源能被正确关闭。
FileInputStream inputStream = null;
try {
创建了一个名为 file 的 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);
}
}
}
}

避免资源的泄漏和浪费。
File file = new File("./tmp.txt");
在 try 语句块中进行这些操作,以便在后续的代码中可以使用这个输入流来读取文件内容。
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}

2. 指定具体的异常

使用最具体的异常来声明方法,这样能让代码更易理解。

不应该做这个事情。会抛出异常。具体来说,就是在执行这个方法时可能会出现异常情况,需要进行相应的处理。
...
}
公共的 void 类型的 doThis()方法会抛出 NumberFormatException 异常。
...
}

如上,从字面上看,NumberFormatException 能够表明是数字格式化方面出现的错误。

3. 对异常进行文档说明

当在方法上声明要抛出异常时,就需要进行文档方面的说明。如同前面的那一点一样,其目的都是为了给调用者提供较多的信息,这样就能更好地避免或处理异常。这篇文章还推荐看下关于异常处理的 10 个最佳实践。

在 Javadoc 里添加 throws 声明,同时对抛出异常的场景进行描述。

/**
这种方法能做一件极为有用的事情……
*
* @param input
如果……发生,就会抛出 MyBusinessException 。
*/
公共的 void 做某件事(String 输入)抛出 MyBusinessException {
...
}

4. 抛出异常的时候包含描述信息

抛出异常时,要尽可能精准地描述问题及相关信息。这样,无论是打印到日志里,还是在监控工具中,都能更便于人阅读,进而能更好地定位具体错误信息以及错误的严重程度等。

但这里并非要对错误信息进行冗长的论述,因为 Exception 的类名本身就能体现错误的缘由,所以只需用一两句话来进行描述就可以了。

try {
new Long("xyz");
如果捕获到 NumberFormatException 异常,就会执行这里的代码。
log.error(e);
}

这个异常是格式化错误,由 NumberFormatException 告知。异常的额外信息只需提供这个错误字符串。当异常的名称不够明显时,就需要提供尽可能具体的错误信息。

5. 首先捕获最具体的异常

现在很多集成开发环境(IDE)都具备智能提示此最佳实践的功能。当你尝试首先去捕获最宽泛的异常时,它会提示你这样做无法达到预期的代码效果。在存在多个 catch 块的情况下,按照捕获的先后顺序,只有第一个匹配到的 catch 块才能够被执行。所以,如果先捕捉到 IllegalArgumentException ,就无法运行到对 NumberFormatException 的捕捉。

需要先捕获最具体的异常。
try {
doSomething("A message");
如果捕获到了 NumberFormatException 异常,就会执行这里的代码。
log.error(e);
当捕获到的异常是 IllegalArgumentException 这种类型时,会执行 catch 部分的代码逻辑;
log.error(e)
}
}

6. 不要捕获Throwable

Throwable 是所有异常和错误的父类。你能够在 catch 语句中进行捕获,然而绝对不要这么去做。倘若在 catch 中捕获了 throwable,那么不但会捕获所有的 exception,同时也会捕获 error。而 error 意味着是无法恢复的 jvm 错误。因此,除非绝对有把握能够处理或者被要求处理错误,就不要去捕获可抛出的异常。

它专注于不处理可能抛出的异常情况。
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}

7. 不要忽略异常

很多时候,开发者对自己很有信心,认为不会抛出异常。所以他们写了一个 catch 块。然而,在这个 catch 块中,他们没有进行任何处理,也没有记录日志。

需要重视异常情况,不能对其进行忽略。在程序执行过程中,可能会遇到各种异常情况,我们不能忽视它们,而应该积极地处理这些异常,以确保程序的稳定性和可靠性。
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}

但现实情况是常常会有无法预料的异常出现,也可能无法确定这里的代码在未来是否会被改动(删除了阻止异常抛出的代码)。在这种情况下,因为异常被捕获了,所以无法获取到足够的错误信息来对问题进行定位。合理的做法是至少要把异常的信息记录下来。

通过这种方式,可以更好地跟踪和处理异常情况。
try {
// do something
} catch (NumberFormatException e) {
log 输出错误信息,内容为“这绝不应该发生:”加上 e ; 表示这种情况不应该出现却发生了; log 记录了一个错误,其内容是不该发生的事情与 e 相关; log 报错,内容是这种情况本不应发生且包含 e ; log 记录了一个不该发生的错误,其中包含 e ; log 出现错误,其信息为这种情况不该发生且伴有 e ; log 记录了一个不该出现的错误,该错误与 e 有关; log 输出了一个错误,此错误为不该发生的情况且包含 e ; log 记录了因不该发生的情况而产生的错误,错误中包含 e ; log 记录了一个因不该发生而导致的错误,该错误与 e 相关联; log 记录了一个由于不该发生的状况而引发的错误,此错误包含 e ; log 记录了一个因不该出现的情况而产生的错误,该错误与 e 有联系; log 记录了一个由不该发生的情形而导致的错误,此错误包含 e ; log 记录了一个由于不该有的情况而引发的错误,该错误与 e 相关; log 记录了一个因不该出现的状况而产生的错误,此错误包含 e 相关的内容; log 记录了一个由不该发生的情况而导致的错误,此错误包含与 e 相关的部分; log 记录了一个因不该出现的情形而引发的错误,此错误包含与 e 有关的内容; log 记录了一个由于不该有的状况而产生的错误,此错误包含与 e 相关联的部分; log 记录了一个因不该发生的情况而导致的错误,此错误包含与 e 有联系的内容; log 记录了一个由于不该出现的情形而引发的错误,此错误包含与 e 相关的要素; log 记录了一个因不该有的状况而产生的错误,此错误包含与 e 相关的部分内容; log 记录了一个由不该发生的情况而导致的错误,此错误包含与 e 相关的一些内容; log 记录了一个由于不该出现的情形而引发的错误,此错误包含与 e 相关的部分信息; log 记录了一个因不该有的状况而产生的错误,此错误包含与 e 相关的一些要素; log 记录了一个由不该发生的情况而导致的错误,此错误包含与 e 相关的部分细节; log 记录了一个由于不该出现的情形而引发的错误,此错误包含与 e 相关的部分情况; log 记录了一个因不该有的状况而产生的错误,此错误包含与 e 相关的部分状况信息; log 记录了一个由不该发生的情况而导致的错误,此错误包含与 e 相关的部分状况细节;
}
}

8. 不要记录并抛出异常

可以发现,在很多代码以及类库当中,都存在着捕获异常、记录日志并且再次抛出的这种逻辑。具体如下:

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

这个处理逻辑看起来是合理的。然而,这种情况经常会导致给同一个异常输出多条日志。具体如下:

17 时 44 分 28 秒 945 出现错误 TestExceptionHandling,在第 65 行 - 发生了 java.lang.NumberFormatException 异常,原因是输入字符串为 "xyz"
在主线程中出现异常。异常类型为 java.lang.NumberFormatException。原因是输入字符串为 "xyz",该字符串无法转换为有效的数字格式。
在 java.lang.NumberFormatException 的 forInputString 方法处(NumberFormatException.java:65)
在 java.lang.Long 类的 parseLong 方法中,位于 Long.java 文件的第 589 行。
在 java.lang.Long 类的 Long.java 文件的第 965 行。
在 com.stackify.example.TestExceptionHandling 类的 TestExceptionHandling.java 文件的第 63 行,进行了 logAndThrowException 操作。
在 com.stackify.example.TestExceptionHandling 的 main 方法中,位于 TestExceptionHandling.java 文件的第 58 行。

后面的日志如所示,没有附加更有用的信息。若要提供更有用的信息,可将异常包装为自定义异常。

公共的方法用于包装异常,该方法接收一个字符串类型的参数 input,在执行过程中如果出现异常,会抛出 MyBusinessException 类型的异常。
try {
// do something
} catch (NumberFormatException e) {
抛出一个新的 MyBusinessException 异常,异常消息为 "A message that describes the error.",同时携带异常 e。
}
}

因此,只有在想要处理异常的时候才去进行捕获。另外,在其他情况下,只需在方法签名中进行声明,让调用者去处理异常即可。

9. 包装异常时不要抛弃原始的异常

捕获标准异常并将其包装为自定义异常是常见的做法。这种做法能够添加更具体的异常信息,还可以进行针对性的异常处理。

当包装异常时,务必将原始异常设置为 cause(因为 Exception 有可传入 cause 的构造方法)。不然的话,丢失原始异常信息会致使错误分析变得困难。

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

总结

综上可知,抛出或捕获异常时,有诸多不同的东西需考虑。其中许多点旨在提升代码的可阅读性或 API 的可用性。异常不单是错误控制机制,还是沟通媒介,所以与协作者讨论这些最佳实践并制定规范,能让大家理解相关通用概念且按相同方式使用它们。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读