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

翻译自:https://dzone.com/articles/9-best-practices-to-handle-exceptions-in-java


作者注:无论你是一名新手或者是一名有经验的专业人士,经常温习一下异常处理的优秀实践能让你和你的团队更好的解决异常相关问题。

Java中的异常处理不是一个简单的主题。初学者觉得它难以理解,甚者有经验的开发者也需要花费数小时时间讨论如何抛出或者处理某一个异常。
因此,大多数开发团队都会有自己的异常处理规则。如果你刚刚加入到团队,你会惊奇的发现这些规则与你之前使用的是多么的不同。
不同团队使用的异常处理的优秀实践有很多。下面的九种最佳实践能够能让你更好的认识异常处理或者提升异常处理的能力。

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-resrouce语句。

使用finally语句块
与上面try语句块的最后几行相比,无论try语句块正常执行或者你在catch语句块中执行异常处理,finally语句块确保语句总是得以执行。因此,你可以确保打开的资源得以清理。

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’s try-with-resource语句
另一个选择是使用try-with-resource语句块,在Java异常处理简介中有详细的介绍。
如果你的资源类实现了AutoCloseable接口,你便可以使用try-with-resource语句块。大多数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而不是IllegalArumentExcetion。同时,尽量避免抛出不具体的异常。

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能够帮助你践行本条最佳实践。当你优先捕获匹配信息较少的异常时,IDE会提示代码可能得不到执行。
问题的关键在于,只有第一条匹配异常的catch语句块得以执行。因此,如果你优先捕获IllegalArgumentException异常,那么更具体的异常NumberFormatException处理代码永远得不到执行,因为NumberFromatException是IllegalArgumentException的子类。
牢记优先捕获最为匹配的异常,将不具体的异常放到捕获列表的后面。
如下面的代码片,第一个catch语句捕获处理NumberFormatExceptions,然后是IllegalArgumentExceptions。

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。它们都是由于程序不可控导致的错误,且不应该由程序捕获处理。
因此,除非你确信你能够或者需要处理错误,最好不要捕获Throwable。

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

7.不要忽略异常
你曾经分析过只执行一部分用例的错误报告吗?
这往往是由于忽略异常引起的。开发者可以十分确定代码不会抛出异常并且添加了不做任何处理或者仅记录日志的捕获异常处理。当你看到这段代码时,你会发现那句有名的注释:“这永远不会发生”。

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)

这些额外的信息没有添加任何有用的信息。正如在第四条最佳实践中所说的,异常消息应该详细描述异常事件。调用栈已经告诉你所抛出异常的类、方法和代码行。
如果你需要添加额外的信息,你应该捕获异常然后重新包装它。但确保遵守第九条最佳实践。

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的易用性。
异常处理同时涉及错误处理机制和事件传递机制。因此,你应该与你的同事讨论异常处理的最佳实践,从而让每个人理解异常的基本概念并采用相同的方法使用异常处理。

PS:简书地址链接:https://www.jianshu.com/p/1948dc648f15

猜你喜欢

转载自blog.csdn.net/conanswp/article/details/79477053