在 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-with-resource
语句。
使用 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);
}
}
}
}
新的 Try-With-Resource 语句
另一种选择是 try-with-resource
语句,我们在 Java 异常处理简介中对此进行了更详细的解释。
如果我们的资源实现了
AutoCloseable
接口,则可以使用它。 这就是大多数 Java 标准资源所做的。
当我们在 try
子句中打开资源时,它会在执行 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. 使用描述性消息抛出异常
这个最佳实践背后的想法与前两个相似。但这一次,我们不向方法的调用者提供信息。
当日志文件或您的监控工具报告异常时,必须了解发生了什么的每个人都会阅读异常的消息。
因此
,它应该尽可能准确地描述问题并提供最相关的信息以了解异常事件。
不要误会我的意思;你不应该写一段文字。但是你应该用 1-2 个简短的句子来解释异常的原因。
这有助于的运营团队了解问题的严重性,还可以让大家更轻松地分析任何服务事件。
如果你抛出一个特定的异常,它的类名很可能已经描述了错误的种类。因此,我们不需要提供很多额外信息。
NumberFormatException
就是一个很好的例子。当我们以错误的格式提供String
时,类java.lang.Long
的构造函数会抛出它。
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
块被执行。
因此
,如果我们先捕获IllegalArgumentException
,那么我们将永远不会到达应该处理更具体的NumberFormatException
的catch
块,因为它是IllegalArgumentException
的子类。
始终首先捕获最具体的异常类,然后将不太具体的 catch
块添加到列表的末尾。
我们可以在以下代码片段中看到此类 try-catch
语句的示例。 第一个 catch 块处理所有 NumberFormatExceptions
,第二个 catch
块处理所有非 NumberFormatException
的 IllegalArgumentExceptions
。
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
6. 不要捕获 Throwable
Throwable
是所有异常和错误的超类。 你可以在 catch
子句中使用它,但你永远不应该这样做!
如果在 catch 子句中使用 Throwable,它不仅会捕获所有异常; 它还会捕获所有错误。
错误由 JVM 抛出以指示不打算由应用程序处理的严重问题。
典型的例子是 OutOfMemoryError
或 StackOverflowError
。 两者都是由应用程序无法控制且无法处理的情况引起的。
因此
,最好不要捕获 Throwable,除非你完全确定自己处于能够或需要处理错误的特殊情况。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
7. 不要忽略异常
你是否曾经分析过仅执行了用例的第一部分的错误报告?
这通常是由被忽略的异常引起的。 开发人员可能非常确定它永远不会被抛出并添加了一个不处理或记录它的 catch
块。
当你找到这个块时,你很可能甚至会发现著名的“这永远不会发生”评论之一:
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 的可用性。
异常通常同时是错误处理机制和通信媒介。
因此,我们应该确保与我们的同事讨论我们希望应用的 Java 异常处理最佳实践和规则,以便每个人都能理解一般概念并以相同的方式使用它们。
相关文章
Do you understand JavaScript closures?
发布时间:2025/02/21 浏览次数:108 分类:JavaScript
-
The function of a closure can be inferred from its name, suggesting that it is related to the concept of scope. A closure itself is a core concept in JavaScript, and being a core concept, it is naturally also a difficult one.
Do you know about the hidden traps in variables in JavaScript?
发布时间:2025/02/21 浏览次数:178 分类:JavaScript
-
Whether you're just starting to learn JavaScript or have been using it for a long time, I believe you'll encounter some traps related to JavaScript variable scope. The goal is to identify these traps before you fall into them, in order to av
How much do you know about the Prototype Chain?
发布时间:2025/02/21 浏览次数:150 分类:JavaScript
-
The prototype chain can be considered one of the core features of JavaScript, and certainly one of its more challenging aspects. If you've learned other object-oriented programming languages, you may find it somewhat confusing when you start
如何在 JavaScript 中合并两个数组而不出现重复的情况
发布时间:2024/03/23 浏览次数:86 分类:JavaScript
-
本教程介绍了如何在 JavaScript 中合并两个数组,以及如何删除任何重复的数组。