java学习网 java教程视频 java论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 724|回复: 0
收起左侧

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

[复制链接]

506

主题

551

帖子

3439

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
3439
发表于 2020-7-19 10:54:30 | 显示全部楼层 |阅读模式
640?wx_fmt=png.jpg

在Java中进行处理异常并非是一件容易的事,初学者经常陷入困惑,甚至有经验的开发者也需要认真研讨哪些异常需要处理,哪些异常需要向上抛出。导致每个开发团队都会自己定制一套特有的异常处理规则,这使得新加入团队的成员都经历一段痛苦的适应期。
尽管如此,前辈们依然总结了几个最佳实践可以遵循,这些实践被绝大多数的团队所采用,本文将为你列出9个最常用且最重要的实践来帮助你提升异常处理的技能。
在做任何事的行动之前,知道为什么做?做了能解决什么问题?然后才去思考怎么做!这样不仅会让你思路更清晰,还可以让这件事更有价值。因此在进入探讨异常处理最佳实践的正题之前,我们首先需要解决两个问题:
  • 什么是异常和异常处理?
  • 为什么需要它们?

异常及异常处理

什么是异常?总结为一句话就是:程序在执行过程中产生的异常情况。当某些事情出现了错误异常就会发生,比如打开一个并不存在的文件、尝试在一个为null的对象上调用方法等等,都会发生异常。
异常是不可预知的,可是一旦它发生了就需要进行异常处理,可谓知错就改善莫大焉!异常处理是一种错误处理机制,如果你不对异常做任何处理,异常将会导致应用程序崩溃。
640?wx_fmt=gif.jpg
一旦你选择了进行处理异常,也就意味着你承认问题的发生,采用必要要的措施去让应用程序从错误中恢复,从而让业务继续进行,阻止应用程序崩溃。
640?wx_fmt=jpeg.jpg
实际上异常处理并不是处理问题的唯一一种方式,如今的高级语言一般都有异常处理机制,但比较古老的如C语言是通过返回错误码的方式来处理异常的。比如数组越界比较常用的返回值是-1。
这种方式的优点是代码逻辑易于推理,没有中断和代码跳转。另一方面,这种处理方式鼓励函数的调用者总是检查返回的错误码。但是这种检查容易造成代码污染,导致代码的可读性和可维护性降低。
错误代码的另一个严重的缺点是缺乏上下文信息,你可能知道错误码“-5”代表找不到文件,但究竟找不到哪个文件呢!错误码就无法表述了。
640?wx_fmt=gif.jpg
错误代码一般用于面向过程的语言,对面向对象的高级语言,有些场景是无能为力的,比如构造函数异常,是无法返回错误码的。

异常处理
640?wx_fmt=png.jpg

当异常被抛出时,应用程序的流程就会被中断,如果没能及时处理异常,应用程序将崩溃。用户将看到异常信息,但那些信息大多他们是看不懂的,这将是一个很糟糕的用户体验,实际上异常信息还可以包装成非常友好的提示。
所以必须进行异常处理,哪怕是为了提高用户体验、记录问题日志、优雅地退出应用程序等。
我们可以使用代码块(try...catch...)来进行异常处理,当发生异常,通过try执行代码,如果发生异常,应用程序的流程将转移到catch中,catch捕捉到异常并进行必要的处理。
以上表述的异常处理原理对初学者依然比较抽象,我们来举个例子


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

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


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异常体系
640?wx_fmt=png.jpg

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

640?wx_fmt=jpeg.jpg


最佳实践
640?wx_fmt=png.jpg


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

        // 使用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关闭所有打开的资源。


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语句块执行完毕后,会自动关闭资源,代码也会简洁许多。如下


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类。


//不建议
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 异常携带可描述的信息

这条最佳实践和前面两条有点相似,但这条提供的信息不单是给方法调用者看的,而更多的是为了给记录日志或监控工具提供的,便于排查异常。实际上一般的异常类名就已经描述了问题的类型,你不必提供大量的附加信息,简洁凝练即可。比如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"

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


/**
* 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,因此最佳时间是总是先捕捉更多信息的异常(子类),再捕捉父类。如


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抛出的严重错误,应用程序对这类错误是无能为力的。


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

7 不要忽略异常

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


public void doNotIgnoreExceptions() {
    try {
        // do something
    } catch (NumberFormatException e) {
        // this will never happen,I'm sure!!
    }
}


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


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

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

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


try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}


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


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 不消费包装异常

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


public void wrapException(String input) throws MyBusinessException {
try {
  // do something
} catch (NumberFormatException e) {
  throw new MyBusinessException("A message that describes the error.", e);
}
}
总结
640?wx_fmt=png.jpg

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

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

顶部qrcode底部
请扫码本站微信公众号

QQ|Archiver|手机版|sitemap.xml|java教程视频 ( 豫ICP备2020032868号 )

GMT+8, 2020-12-4 04:27

Powered by www.javaj.cn

Copyright © 2001-2020.

快速回复 返回顶部 返回列表