Java 处理异常的 9 个最佳实践

无论您是新人还是老手,复习异常处理实践总是好的,以确保您和您的团队能够处理问题。

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);
            }
        }
    }
}

Java 7 的 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。并避免抛出不特定的Exception

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就是一个很好的例子。当您提供格式错误的字符串时,它会被类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 块处理所有NumberFormatException,第二个处理所有IllegalArgumentException,它们不是NumberFormatException

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 抛出错误以指示不打算由应用程序处理的严重问题。典型的例子是OutOfMemoryErrorStackOverflowError。两者都是由应用程序无法控制且无法处理的情况引起的。

因此,最好不要捕获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 的可用性。

异常通常同时是一种错误处理机制和一种通信媒介。因此,您应该确保与您的同事讨论您想要应用的最佳实践和规则,以便每个人都理解一般概念并以相同的方式使用它们。

想学习更多知识与技巧点赞三连,关注私信博主哟

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值