前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java中异常处理的9个最佳实践

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

作者头像
码农神说
发布2020-08-05 16:08:41
6070
发布2020-08-05 16:08:41
举报
文章被收录于专栏:码农神说

在Java中进行处理异常并非是一件容易的事,初学者经常陷入困惑,甚至有经验的开发者也需要认真研讨哪些异常需要处理,哪些异常需要向上抛出。导致每个开发团队都会自己定制一套特有的异常处理规则,这使得新加入团队的成员都经历一段痛苦的适应期。

尽管如此,前辈们依然总结了几个最佳实践可以遵循,这些实践被绝大多数的团队所采用,本文将为你列出9个最常用且最重要的实践来帮助你提升异常处理的技能。

在做任何事的行动之前,知道为什么做?做了能解决什么问题?然后才去思考怎么做!这样不仅会让你思路更清晰,还可以让这件事更有价值。因此在进入探讨异常处理最佳实践的正题之前,我们首先需要解决两个问题:

  1. 什么是异常和异常处理?
  2. 为什么需要它们?

异常及异常处理

什么是异常?总结为一句话就是:程序在执行过程中产生的异常情况。当某些事情出现了错误异常就会发生,比如打开一个并不存在的文件、尝试在一个为null的对象上调用方法等等,都会发生异常。

异常是不可预知的,可是一旦它发生了就需要进行异常处理,可谓知错就改善莫大焉!异常处理是一种错误处理机制,如果你不对异常做任何处理,异常将会导致应用程序崩溃。

一旦你选择了进行处理异常,也就意味着你承认问题的发生,采用必要要的措施去让应用程序从错误中恢复,从而让业务继续进行,阻止应用程序崩溃。

实际上异常处理并不是处理问题的唯一一种方式,如今的高级语言一般都有异常处理机制,但比较古老的如C语言是通过返回错误码的方式来处理异常的。比如数组越界比较常用的返回值是-1。

这种方式的优点是代码逻辑易于推理,没有中断和代码跳转。另一方面,这种处理方式鼓励函数的调用者总是检查返回的错误码。但是这种检查容易造成代码污染,导致代码的可读性和可维护性降低。

错误代码的另一个严重的缺点是缺乏上下文信息,你可能知道错误码“-5”代表找不到文件,但究竟找不到哪个文件呢!错误码就无法表述了。

错误代码一般用于面向过程的语言,对面向对象的高级语言,有些场景是无能为力的,比如构造函数异常,是无法返回错误码的。

异常处理

当异常被抛出时,应用程序的流程就会被中断,如果没能及时处理异常,应用程序将崩溃。用户将看到异常信息,但那些信息大多他们是看不懂的,这将是一个很糟糕的用户体验,实际上异常信息还可以包装成非常友好的提示。

所以必须进行异常处理,哪怕是为了提高用户体验、记录问题日志、优雅地退出应用程序等。

我们可以使用代码块(try...catch...)来进行异常处理,当发生异常,通过try执行代码,如果发生异常,应用程序的流程将转移到catch中,catch捕捉到异常并进行必要的处理。

以上表述的异常处理原理对初学者依然比较抽象,我们来举个例子

代码语言:javascript
复制
package com.zqf;

public class App
{
    public static void main(String[] args){
        System.out.println("First line");
        System.out.println("Second line");
        System.out.println("Third line");
        //初始化具有3个元素的素组
        int[] myIntArray = new int[]{1, 2, 3};
        print4thItemInArray(myIntArray);
        System.out.println("Fourth line");
        System.out.println("Fith line");
    }

    private static void print4thItemInArray(int[] arr) {
        //获取第4个(下标3)元素,因为没有所以抛出异常
        System.out.println(arr[3]);
        System.out.println("Fourth element successfully displayed!");
    }
}

分析下这个程序,在main中初始化有3个元素的数组,把这个数组传递给私有方法print4thItemInArray,在print4thItemInArray中试图获取数组的第4个元素,由于没有第4个元素将抛出“ArrayIndexOutOfBoundsException”异常,应用程序只会打印到“Third line”。

执行应用输出结果如下

First line Second line Third line Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3 at com.zqf.App.print4thItemInArray(App.java:20) at com.zqf.App.main(App.java:14) Process finished with exit code 1

现在我们修改下,增加异常处理

代码语言:javascript
复制
package com.zqf;

public class App {
    public static void main(String[] args) {
        System.out.println("First line");
        System.out.println("Second line");
        System.out.println("Third line");
        //初始化具有3个元素的素组
        int[] myIntArray = new int[]{1, 2, 3};

        try {//捕捉异常
            print4thItemInArray(myIntArray);
        }catch (ArrayIndexOutOfBoundsException ex){//异常处理
            System.out.println("Have no four items!");
        }
        System.out.println("Fourth line");
        System.out.println("Fith line");
    }

    private static void print4thItemInArray(int[] arr) {
        //获取第4个(下标3)元素,因为没有所以抛出异常
        System.out.println(arr[3]);
        System.out.println("Fourth element successfully displayed!");
    }
}

现在运行看看输出

First line Second line Third line Have no four items! Fourth line Fith line

实际上这次的异常依然会发生,因为第4个元素的确不存在,所以在"Fourth element successfully displayed!"输出之前就抛出了异常,中断执行流程,但流程跳转到catch语句块了,catch只打印了一条“Have no four items”,继续向下执行。

Java异常体系

在Java中,所有的异常都有一个共同的祖先Throwable,它有2个子类:Exception(异常)和Error(错误),它们又各自有大量的子类。Exception(异常)和Error(错误)的共性和区别:两者都可以被捕捉,但前者可以被应用程序本身处理,后者是严重的,是无法恢复处理的。

最佳实践

1

用Finally或Try-With-Resource清理资源

我们经常在try语句块使用资源,比如InputStream,使用完后需要关闭。经常犯的错误是在try语句块中关闭资源。如

代码语言:javascript
复制
public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);

        // 使用inputStream读取文件

        // 不要这样做
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

这种方式看似非常完美,不会有异常抛出,所有的语句在try中执行,关闭IputStream释放资源。但试想一下:如果在“inputStream.close()”语句之前就抛出异常,会怎样呢?正常的流程会被中断并跳转,导致InputStream根本没关闭。

因此,应该把清理资源的代码放在finally或try-with-resource语句中。不管是正常执行完try语句块,还是异常处理完毕,都会执行finally语句块,而你需要确保在finally关闭所有打开的资源。

代码语言:javascript
复制
public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);

        // 使用inputStream读取文件
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (Exception e) {
                log.error(e);
            }
        }
    }
}

在JDK7引入了try-with-resource的语法,简单来说当一个资源对象(如InputSteam对象)实现了AutoCloseable接口,那么就可以在try关键字后的括号里创建实例,当try-catch语句块执行完毕后,会自动关闭资源,代码也会简洁许多。如下

代码语言:javascript
复制
public void doNotCloseResourceInTry() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file)) {

        // 使用inputStream读取文件
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

2

抛出具体异常

你抛出的异常越具体越好,不熟悉你代码的同事或者几个月之后的你,可能需要调用你这些方法并进行异常处理,所以尽可能多的提供信息,让你的API更容易理解,比如能用NumberFormatException就不要用 IllegalArgumentException,绝对避免直接使用不具体的Exception类。

代码语言:javascript
复制
//不建议
public void doNotDoThis() throws Exception { ... }
//建议
public void doThis() throws NumberFormatException { ... }

3

做好注释/文档

只要你在方法声明异常,就需要做好Javadoc的注释。这点和上一条最佳实践有相同的目标:提供给调用者尽可能多的信息,便于避免异常或进行异常处理。所以请确保你在Javadoc中添加了"@throws"声明,并且描述了造成异常的情况。

代码语言:javascript
复制
/**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException if ... happens
*/
public void doSomething(String input) throws MyBusinessException { ... }

4

异常携带可描述的信息

这条最佳实践和前面两条有点相似,但这条提供的信息不单是给方法调用者看的,而更多的是为了给记录日志或监控工具提供的,便于排查异常。实际上一般的异常类名就已经描述了问题的类型,你不必提供大量的附加信息,简洁凝练即可。比如NumberFormatException,当java.lang.Long构造函数抛出异常时会提供一句简短且清晰的文本来描述。

代码语言:javascript
复制
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"

如果你仔细看下JDK的源码,就会清楚java.lang.Long在构造器中做了各种校验,当某些校验失败会调用NumberFormatException.forInputString,而静态方法forInputString会把java.lang.Long的构造参数格式化后再构造一个新的NumberFormatException实例并抛出

代码语言:javascript
复制
/**
 * Factory method for making a <code>NumberFormatException</code>
 * given the specified input which caused the error.
 *
 * @param   s   the input causing the error
 */
static NumberFormatException forInputString(String s) {
    return new NumberFormatException("For input string: \"" + s + "\"");
}

5

线捕捉子类异常

很多IDE都会帮助你进行最佳实践,如果你先捕捉父类异常再捕捉子类异常,它们会告诉你后面的代码不可到达或者警告已经被捕捉,因为是按照catch在在代码中顺序执行的。

所以如果先捕捉IllegalArgumentException,将不能捕捉到其子类NumberFormatException,因此最佳时间是总是先捕捉更多信息的异常(子类),再捕捉父类。如

代码语言:javascript
复制
public void catchMostSpecificExceptionFirst() {
    try {
        doSomething("A message");
    } catch (NumberFormatException e) {
        log.error(e);
    } catch (IllegalArgumentException e) {
        log.error(e)
    }
}

6

不要捕捉Throwable

从Java异常体系的图中可知,Throwable是所有异常(Exception)和错误(Error)的祖先,Throwable是可以被捕捉,但请不要捕捉。如果你捕捉了Throwable,那么不仅仅是捕捉了异常,还捕捉了错误。但错误是无法恢复,它是被JVM抛出的严重错误,应用程序对这类错误是无能为力的。

代码语言:javascript
复制
//不要捕捉Throwable
public void doNotCatchThrowable() {
    try {
        // do something
    } catch (Throwable t) {
        // don't do this!
    }
}

7

不要忽略异常

你是否记得曾几何时,在分析bug时遇到代码只执行了前半部分,但却不知为何。有些开发者经常捕捉了异常,但凭经验认为异常决定不可能发生,导致没有做异常处理。

代码语言:javascript
复制
public void doNotIgnoreExceptions() {
    try {
        // do something
    } catch (NumberFormatException e) {
        // this will never happen,I'm sure!!
    }
}

实际上在大多数情况下它都发生了,因为随着时间和业务逻辑的变更,try代码块的内容变更了,导致了异常发生,而你的自信不仅害了你也害了后来人。建议catch中至少要留一条日志,来告知异常问题,方便排查。

代码语言:javascript
复制
public void logAnException() {
    try {
        // do something
    } catch (NumberFormatException e) {
        log.error("This should never happen: " 
         + e + ",but I get a mistake!");
    }
}

8

不要在仅仅记录日志后向上抛出异常

“不要在仅仅记录日志后向上抛出异常”,这是最佳实践中最容易被忽视的一条。你会发现在大量的代码片段,甚至类库中经常捕捉异常、记录日志,然后抛出异常。

代码语言:javascript
复制
try {
 new Long("xyz");
} catch (NumberFormatException e) {
 log.error(e);
 throw e;
}

直观的感觉是记录异常,然后抛出异常让调用者可以恰当的处理,但同一个异常多处日志记录,会让人迷惑,请参考第4条最佳实践:简洁凝练。

代码语言:javascript
复制
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)

如果你真的需要给调用者提供更多信息,可以参考下一条最佳实践:包装异常。

9

不消费包装异常

比较可取的做法是捕捉到标准异常,根据实际业务自定义包装异常再向上抛出。在包装异常时通常把原始异常作为构造参数传进来,否则会丢失栈的跟踪信息,造成分析困难。

代码语言:javascript
复制
public void wrapException(String input) throws MyBusinessException {
 try {
  // do something
 } catch (NumberFormatException e) {
  throw new MyBusinessException("A message that describes the error.", e);
 }
}

总结

如你所见,当你处理异常或抛出异常是有许多要考虑的事情,大多是从代码的可读性和API可用性来考虑。因此,最好和同事一起讨论异常处理的最佳实践,从而达成共识、步调一致,不仅提高工作效率,还能避免不可预知的异常。

End

版权归@码农神说所有,转载须经授权,翻版必究

转载可联系助手,微信号:codeceo-01

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农神说 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档