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 相比,无论是 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 的 Try-With-Resource 语句

你还可以选择 try-with-resource 语句,在我的这篇Java 异常处理入门中有更为详细的介绍。

如果你在资源中实现了 AutoCloseable 接口的话,就可以使用 try-with-resource 语句了,这也是大多数 Java 标准资源的做法。如果你在 try-with-resource 中打开了一个资源,在 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 (注:例如将参数转换为数值出错时,应该抛出具体的 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、将描述信息与异常一同抛出

这个方法背后的思想和前两个是类似的。但这一次,你不必给你的方法调用者提供信息。对于任何遭遇异常错误并需要搞清楚错误原因的人来说,异常信息总是在异常出现的同时,被记录在了日志中,或打印在了屏幕上。

因此,请尽可能精确地描所以,最好不要在 catch 中使用 Throwable ,除非你能确保自己处于一些特定情况下,比如你自己足以处理错误,又或被要求处理错误时。述异常事件,并提供最相关的信息以令其他人能够理解发生了什么异常时。

别误会我的意思了。你没必要去写上一大段的文字,但你应该用一两句简短的话来解释一下异常发生的原因。这能让你的开发团队明白问题的严重性,也能让你更容易地分析服务事故。

如果你抛出了一个特定的异常,它的类名很可能就已经描述了这是什么类型的错误了。所以,你不需要提供很多额外的描述信息。一个很好的例子是,当你提供了一个错误格式的 String 类型参数时,java.lang.Long 构造函数就会抛出 NumberFormatException 。

try {

    new Long("xyz");

} catch (NumberFormatException e) {

    log.error(e);

}

NumberFormatException 的类名已经告诉了你问题的类型。所以异常信息只需要返回导致问题的输入字符串就行了。如果异常类的名字不能表明其含义,那么你还需要在异常信息中提供必要的解释信息。

1

17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"

5、优先捕获具体的异常

大多数 IDE 都能帮你做到这点。当你尝试优先捕获不那么具体的异常时, IDE 会报告给你这是一个不能到达的代码块。

这个问题的原因是只有第一个匹配到异常的 catch 块才会被执行。所以,如果你先 catch 了一个 IllegalArgumentException ,你将永远无法到达处理更具体异常 NumberFormatException 的 catch 块中,因为 NumberFormatException 是 IllegalArgumentException 的子类。

所以,请优先捕获更具体的异常,并把不那么具体的 catch 块放在后面。

在下面你可以看到这样的一个 try-catch 语句示例。第一个 catch 处理所有的 NumberFormatExceptions 异常,第二个 catch 处理 NumberFormatException 异常以外的 illegalargumentexception 异常。

public void catchMostSpecificExceptionFirst() {

    try {

        doSomething("A message");

    } catch (NumberFormatException e) {

        log.error(e);

    } catch (IllegalArgumentException e) {

        log.error(e)

    }

}

6、不要捕获 Throwable

Throwable 是所有 exceptions 和 errors 的父类。虽然你可以在 catch 子句中使用它,但你应该永远别这样做!

如果你在 catch 子句中使用了 Throwable ,它将不仅捕获所有异常,还会捕获所有错误。这些错误是由 JVM 抛出的,用来表明不打算由应用处理的严重错误。 OutOfMemoryError 和 StackOverflowError 就是典型的例子,这两种情况都是由一些超出应用控制范围的情况导致的,无法处理。

所以,最好不要在 catch 中使用 Throwable ,除非你能确保自己处于一些特定情况下,比如你自己足以处理错误,又或被要求处理错误。

public void doNotCatchThrowable() {

    try {

        // do something

    } catch (Throwable t) {

        // don't do this!

    }

}

7、不要忽略异常

你分析过只有用例的第一部分代码被执行的 bug 报告吗?

这通常是由于忽略异常而导致的。开发者可能十分确定这个异常不会被抛出,然后添加了一个无法处理或无法记录这个异常的 catch 。当你找到这个 catch 时,你很可能会发现这么一句著名的注释: “This will never happen”。

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条准则中所述,异常信息应该准确描述异常事件。 Stack Trace (堆栈追踪)会告诉你异常在哪个类、哪个方法、哪个行中被抛出。

如果你需要添加额外的信息,你应该将异常捕获并包装在自定义的的异常中,但要确保遵循下面的第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、包装异常但不要丢弃原始异常

有时候将异常包装成一个自定义异常会比捕捉一个标准异常要更好。一个典型的例子是应用或框架的特定业务异常。这允许你添加额外的信息,也能为你的异常类实现一个特定的处理方法。

当你这么做的时候,一定要确保原始的异常设为 cause 。 Exception 类提供了一系列的特定构造方法,这些方法可以接受 Throwable 作为参数(注:如Exception(String message, Throwable cause))。否则,你将会丢失原始异常的 stack trace 与信息,这会使你分析导致异常的事件变得十分困难。

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程序员开发: 854393687

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 无论你是新手还是资深开发,复习下异常处理的实践总是一件好事,因为这能确保你与你的团队在遇到问题时能够轻松处理它。 ...
    Java资讯库阅读 474评论 0 4
  • 在 Java 中,异常处理是个很麻烦的事情。初学者觉得它很难理解,甚至是经验丰富的开发者也要花费很长时间决定异常是...
    OSC开源社区阅读 1,110评论 3 51
  • Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言。Java 技术具有卓越的通用性、高效性、平台移植性和...
    Java小辰阅读 342评论 0 1
  • 最近总是看到年级轻轻的人就得绝症,忽然有种感想越来越强烈,这个世界,活着就好。 人总是对毫不费力拥有的东西不知道去...
    丫丫0205阅读 1,191评论 0 0
  • 站在山顶上喜欢注视远处更高的山青色的威严俊美的仪表直插云霄 注视脚下一阶阶梯田零零散散凸起的土瓦房注视田地头拱背弯...
    君凉阅读 207评论 21 33