如何优雅的处理异常

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 c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator i = this.listIterator();
    for (Object e : a) {
      i.next();
      i.set((E) e);
    }
}

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

你可能感兴趣的:(如何优雅的处理异常)