在 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 异常处理最佳实践和规则,以便每个人都能理解一般概念并以相同的方式使用它们。
相关文章
在 Java 中获取文件大小
发布时间:2023/05/01 浏览次数:139 分类:Java
-
Java 提供了不同的方法来获取文件的字节大小。 本教程演示了在 Java 中获取文件大小的不同方法。使用 Java IO 的文件类获取文件大小 Java IO 包的 File 类提供了以字节为单位获取文件大小的功能。
Java 中的文件分隔符
发布时间:2023/05/01 浏览次数:108 分类:Java
-
本篇文章介绍了 Java 中的文件分隔符。Java 中的文件分隔符 文件分隔符是用来分隔目录的字符; 例如,Unix 使用 /,Windows 使用 \ 作为文件分隔符。
Java 中的文件过滤器
发布时间:2023/05/01 浏览次数:193 分类:Java
-
本篇文章介绍如何在 Java 中使用 FileFilter。FileFilter 用于过滤具有特定扩展名的文件。 Java内置包IO和Apache Commons IO为FileFilter提供了类和接口来进行文件过滤操作。
Java 获取 ISO 8601 格式的当前时间戳
发布时间:2023/05/01 浏览次数:132 分类:Java
-
本篇文章介绍了 ISO 8601 日期格式、其重要性及其在 Java 中的使用。 它还列出了一些优点来强调为什么应该使用 ISO 格式来表示日期。
在 Java 中获取数组的子集
发布时间:2023/05/01 浏览次数:142 分类:Java
-
本篇文章介绍了几种在 Java 中获取数组子集的方法。使用 Arrays.copyOf() 方法获取数组的子集 使用 Arrays.copyOfRange() 方法获取数组的子集
用 Java 填充二维数组
发布时间:2023/05/01 浏览次数:110 分类:Java
-
二维数组是基于表结构的,即行和列,填充二维数组不能通过简单的添加到数组操作来完成。 本篇文章介绍如何在 Java 中填充二维数组。
计算 Java 数组中的重复元素
发布时间:2023/05/01 浏览次数:202 分类:Java
-
本篇文章介绍Java计算数组中重复元素的方法。计算 Java 数组中的重复元素。我们可以创建一个程序来计算数组中的重复元素。 该数组可以是未排序的,也可以是已排序的。
Java 中 List 和 Arraylist 的区别
发布时间:2023/05/01 浏览次数:90 分类:Java
-
表示为单个单元的一组单个对象称为集合。 在 Java 中,Collection 是一个具有多个已定义接口和类的框架,用于将一组对象表示为一个单元。 它允许我们操纵