如何优雅的处理异常

1.异常的定义

在《java编程思想》中这样定义异常:阻止当前方法或作用域继续执行的问题。

2.异常的体系


在Java中异常被当做对象来处理,根类是java.lang.Throwable类,所有异常类都必须直接或间接继承自Throwable类,Throwable类分为以下两个子类:

  • Error类,它是error类型异常的父类;error类型异常是程序无法处理的异常。一般发生这种异常时,JVM会选择终止程序,因此在我们编写程序时,并不需要关心error类异常
  • Exception类,它是exception类型异常的父类,这类异常就是我们编码时需要注意的异常。而对于Exception类,它又分为以下两大类:
    • 非受检性异常,该类异常都直接继承RuntimeException。
    • 受检性异常,该异常直接继承Exception,该类异常要么捕获处理要么声明抛出。

3.如何抛异常

Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
——Effective Java Item 58

  • 受检性异常:希望调用方处理的异常,或者是自己不知道如何处理的,抛出受检性异常。
  • 非受检性异常:程序执行的过程中可能出现的异常,具有偶然性的,抛出非受检性异常。
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }

    public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(name);
        }
        if (name == null) {
          // name is null 不是必然发生的,具有偶然性,抛出非受检性异常
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            // file不存在,不知道如何处理,程序无法继续,抛出受检性异常,希望调用方来处理
            throw new FileNotFoundException("Invalid file path");
        }
        fd = new FileDescriptor();
        fd.attach(this);
        path = name;
        open(name);
    }

4.如何有效的使用异常

判断是否有效的使用异常,要看是否能够回答以下问题:

  • 什么出了错?

  • 在哪出的错?

  • 为什么出错?

如果你的异常没有回答以上全部问题,那么可能你没有很好地使用它们。在有效使用异常的情况下:

  • 异常类型回答了“什么”出了错;

  • 异常堆栈跟踪回答了“在哪“抛出;

  • 异常信息回答了“为什么“会抛出。

4.1三原则

有三个原则可以帮助你最大限度地使用好异常,这三个原则是:

  • 具体明确

  • 提早抛出

  • 延迟捕获

4.1.1具体明确

  • 抛出具体明确的异常
    java.io包中定义了Exception类的子类IOException,更加细分了FileNotFoundException,EOFException和ObjectStreamException这些IOException的子类。
    每一种都描述了一类特定的I/O错误,分别是:文件丢失,异常文件结尾,错误的序列化对象流。异常越具体,我们的程序就能更好地回答”什么出了错”这个 问题。

  • 捕获异常时尽量明确
    例如:可以通过重新询问用户文件名来处理FileNotFoundException,对于 EOFException,它可以根据异常抛出前读取的信息继续运行。如果抛出的是ObjectStreamException,则程序应该提示用户文件已损坏,应当使用备份文件或者其他文件。

4.1.2提早抛出

  • 异常堆栈信息提供了导致异常出现的方法调用链的精确顺序,以此来精确定位异常出现的现场。
  • 通过在检测到错误时立刻抛出异常来实现迅速失败,可以有效避免不必要的对象构造或资源占用,比如文件或网络连接。同样,打开这些资源所带来的清理操作也可以省却。
  • 只在最先出现异常的地方打印异常日志。

4.1.3延迟捕获

  • 在合适的层面捕获异常,以便你的程序要么可以从异常中有意义地恢复并继续下去;要么能够为用户提供明确的信息,包括引导他们从错误中恢复过来。如果你的方法无法胜任,那么就不要处理异常,把它留到后面捕获和在恰当的层面处理。
    先来看一个bad case:
  /**
  * bad case
  */
  public void read(String filename){
      InputStream in = null;
      try{
          in = new FileInputStream(filename);
      } catch (FileNotFoundException e){
          logger.error(e);
          //throw e;
      }
      in.read(...);
}
  • 上面的代码在完全没有能力从FileNotFoundException中恢复过来的情况下就捕获了它。只能打印日志,然后继续向上抛出异常。当然也可以做类似return null的返回。这些都不够友好,增加了不必要的代码判断和阅读理解上的困难。推荐如下:
  /**
  * good case
  */
  public void read(String filename) throws IllegalArgumentException, FileNotFoundException{
       if (filename == null){
           throw new IllegalArgumentException("filename is null");
       }
       InputStream in = new FileInputStream(filename);
       in.read(...);
  }
  • 我们明确声明了方法可能抛出FileNotFoundException和IllegalArgumentException,虽然IllegalArgumentException不是受检异常(即RuntimeException的子类),声明它是为了文档化我们的代码(这些异常也应该在方法的JavaDocs中标注出来)。FileNotFoundException应该让调用的上游来捕获,让有能力做决定的方法来处理FileNotFoundException,是给用户提示还是查找其他文件or重试等等。

5.异常的设计和处理建议

5.1只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程

  • 因为异常机制的设计初衷是用于不正常情形,所以很少会有JVM实现试图对它们进行优化,把代码放在try-catch块中反而阻止了现代JVM实现本来可能要执行的某些特征优化

5.2避免多次在日志信息中记录同一个异常

  • 只在异常最开始发生的地方进行日志信息记录。很多情况下异常都是层层向上抛出的,如果在每次向上抛出的时候,都Log到日志系统中,则会导致无从查找异常发生的根源。

5.3异常处理尽量放在高层进行

  • 尽量将异常统一抛给上层调用者,由上层调用者进行处理。如果在每个出现异常的地方都直接进行处理,会导致程序异常处理流程混乱,不利于后期维护和异常错误排查。由上层统一进行处理会使得整个程序的流程清晰易懂。

5.4对可恢复的情况使用受检异常,对编程错误使用运行时异常

5.5避免不必须要的使用受检异常

  • 由于每抛出一个受检的异常,使用者都需要去catch它或者再次抛出去,所以如果能避免这种异常则应该尽量避免。

5.6抛出与抽象相对应的异常

  • 在调用高层的API时不应该抛出低层的异常,否则会使人不知所措,同时会暴露低层里面的相关实现细节,这个时候就需要异常转译,比如以AbstractSequentialList实现了List这个接口,它里面的一个get方法就进行了异常转译:
public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}
  • 当然有时候是希望能看到低层的异常,这样可以用于调试,那么你可以使用异常链:
  try{
    //...to something
  }catch(LowerLevelException cause){
    //这里抛出高层的异常中会带有低层异常的信息
    throw new HighLevelException(cause)
  }
  • 异常转译和异常链也不能被滥用,如果可能,处理来自低层异常的最好做法就是在调用低层方法之前确保它们会执行成功,从而避免它们抛出异常,比如在执行低层方法之前先对需要传递参数的检查就是一个好的方法。

5.7在细节消息中包含能捕获的失败信息

  • 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。异常信息中最有用的是那些导致异常的数据,以IndexOutOfBoundsException为例,该异常抛出时就应该有正确的上界,下界以及没有落入界内的下标值,这样就可以非常轻松的让开发人员了解异常出现的原因以及如何去fix它
    private static Object add(Object array, int index, Object element, Class clss) {
        if (array == null) {
            if (index != 0) {
                throw new IndexOutOfBoundsException("Index: " + index + ", Length: 0");
            }
            Object joinedArray = Array.newInstance(clss, 1);

            Array.set(joinedArray, 0, element);
            return joinedArray;
        }
        int length = Array.getLength(array);
        if (index > length || index < 0) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + length);
        }
        Object result = Array.newInstance(clss, length + 1);
        System.arraycopy(array, 0, result, 0, index);
        Array.set(result, index, element);
        if (index < length) {
            System.arraycopy(array, index, result, index + 1, length - index);
        }
        return result;
    }

5.8努力使失败保持原子性

  • 当对象抛出异常之后,通常我们期望这个对象仍然保持在一个定义良好的可用状态之中。一般而言,失败的方法调用应该是对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性。
default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
      i.next();
      i.set((E) e);
    }
}

传送门:阿里的异常使用规范

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

推荐阅读更多精彩内容