中级06 - Java的异常体系

Java的异常体系健壮而安全,比C++高到不知道哪里去了。

什么是异常
Java的异常体系
throw/throws
try/catch/finally


一、异常入门与控制流

  • 如果没有 try,异常将击穿所有的栈帧
  • catch 可以将一个异常抓住
  • finally 执行清理工作
  • JDK7+ try-with-resources

1. 什么是异常

异常为方法提供了另外的出口。
如果当前抛出异常的方法栈不能处理异常,则该异常会顺着方法栈一直往下(外)抛,直到遇到处理该异常的方法。

2. Java7+的try with resources语句

任何实现了 AutoCloseable 接口的,都可以在 try with resources 时被自动关闭。
传统 try finally:

public static void main(String[] args) throws IOException {
    FileInputStream is = null;
    try {
        is = new FileInputStream("");
    } finally {
        if (is != null) {
            is.close();
        }
    }
}

IDEA 中可以看到try被高亮,此时使用 Alt+Enter 将代码替换为 try with resources:

public static void main(String[] args) throws IOException {
    try (FileInputStream is = new FileInputStream("")) {
    }
}

3. throw/throws

throw抛出一个异常,如果异常击穿了整个方法栈(没被任何方法 catch 住),那么该线程会被 kill。
throws只是一个声明,代表未来可能会抛出异常。

public static void foo() throws Exception {
    throw new Exception();
}

二、异常的类型体系

1. Throwable

  • Throwable - 可以被抛出的东西
    • Exception - 受检异常,预料之中
      • RuntimeException 运行时异常,非受检异常,预料之外
    • Error 错误

在 java 中 Throwable 类是所有错误和异常的父类,只有(任何实现了)这个类型的对象或者子类的对象才(都)能被抛出来。因此能被throw出来的并非仅限于内置的异常类型。

Exception 除了 RuntimeException 子类之外,属于预料之中的异常,是 checked exceptions 受检异常,即编辑期需要检查,需要声明throws子句。
而 RuntimeException 属于预料之外的异常,是运行时异常,是 unchecked exceptions 非受检异常,不需要声明throws

Exception 是可以恢复的异常,而 Error 是不能恢复的严重异常,编译期无法预料,所以被视为 unchecked error 非受检。

任何需要父类的地方总是可以传递一个其子类对象,在异常体系中同样适用:

public class Main {
    private static void throwCheckedException() throws Exception {
        if (fileNotFound()) {
            throw new FileNotFoundException();
        } else if (reachFileEnd()) {
            throw new EOFException();
        } else {
            throw new IOException();
        }
    }

    public static void main(String[] args) {
        try {
            throwCheckedException();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

如上所示,throws 声明时只需一个Exception即可,不必 throws 多个(throws FileNotFoundException, EOFException等等)

2. catch的级联与合并

级联:
根据不同异常在 try catch 中分别处理时,应依据顺序从上至下、范围从小到大,保证优先 catch 到匹配最精确的,沿着类型继承体系,逐渐 catch 到层级更通用的异常:

try {
    throwCheckedException();
} catch (NullPointerException e) {
    System.out.println("空指针异常");
} catch (RuntimeException e) {
    System.out.println("运行时异常");
} catch (Exception e) {
    e.printStackTrace();
} catch (Throwable e) {
    e.printStackTrace();
}

合并:
当两个 catch 在做同样的事情时,如果有父子关系可以考虑合并为父类型的,如果不存在父子关系,可以:

try {
    throwCheckedException();
} catch (NullPointerException | IllegalAccessException e) {
    e.printStackTrace();
}

三、异常的栈轨迹

1. 栈轨迹(StackTrace)

排查问题最重要的信息,没有之一。

public class Main {
    private static void b() {
        throw new RuntimeException();
    }
    private static void a() {
        b();
    }
    private static void foo() {
        a();
    }
    public static void main(String[] args) {
        foo();
    }
}
Exception in thread "main" java.lang.RuntimeException
    at com.github.hcsp.controlflow.Main.b(Main.java:6)
    at com.github.hcsp.controlflow.Main.a(Main.java:10)
    at com.github.hcsp.controlflow.Main.foo(Main.java:14)
    at com.github.hcsp.controlflow.Main.main(Main.java:18)

2. 异常链(Caused by)

catch 住一个异常时,将其作为 new 一个异常类型时的 cause 参数,传递给另一个异常(可以是内置的,也可以是自定义的异常类型)并抛出去,形成异常链:

package com.github.hcsp.controlflow;
import java.sql.SQLException;
public class Main {
    // 继承自非受检异常,并且为了演示方便,使用了内部静态类
    private static class UserAlreadyExistException extends RuntimeException {
        public UserAlreadyExistException(String message, SQLException cause) {
            super(message, cause);
        }
    }

    private static void insertIntoDatabase() throws SQLException {
        throw new SQLException("重复的键值");
    }
    // 首先捕获到底层SQL异常,然后使用自定义的错误类型进行二次封装,
    // 以提供更多的错误信息,方便排查
    private static void processUserData() {
        Integer userId = 1;
        try {
            insertIntoDatabase();
        } catch (SQLException e) {
            throw new UserAlreadyExistException("插入id为" + userId + "的数据时发生了异常", e);
        }
    }
    public static void main(String[] args) {
        try {
            processUserData();
        } catch (Exception e) {
            throw new RuntimeException("main方法运行时出异常啦", e);
        }
    }
}

可以看到以Caused by开头打印出的异常链,其中最底下的是最原始的异常:

Exception in thread "main" java.lang.RuntimeException: main方法运行时出异常啦
    at com.github.hcsp.controlflow.Main.main(Main.java:33)
Caused by: com.github.hcsp.controlflow.Main$UserAlreadyExistException: 插入id为1的数据时发生了异常
    at com.github.hcsp.controlflow.Main.processUserData(Main.java:25)
    at com.github.hcsp.controlflow.Main.main(Main.java:31)
Caused by: java.sql.SQLException: 重复的键值
    at com.github.hcsp.controlflow.Main.insertIntoDatabase(Main.java:15)
    at com.github.hcsp.controlflow.Main.processUserData(Main.java:23)
    ... 1 more

四、关于异常的原则

1. 异常的抛出原则

  • 能用 if/else 处理的,不要使用异常
  • 异常要准确、带有详细信息
    比如FileNotFoundException比其父类型 IOException更准确
    并尽量提供当前环境信息
  • 尽早抛出异常,回头是岸
    如果代码无法继续,不要假装看不见,更不要偷偷吞掉(空的 catch 语句),抛出异常远比悄悄执行错误逻辑强得多,反例如下:
private static String request(String cookie) {
    // ...
    try {
        HttpResponse response = client.execute(get);
        result = EntityUtils.toString(response.getEntity());
    }  catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

2. 异常的处理原则

  • 不要处理不归当前方法管的异常
  • 如果当前方法无能力处理,就抛出
  • 如非万分必要,不要忽略异常

3. 了解和使用JDK内置的异常

当然是优先使用内置的异常,别总想着自己造。

  • NullPointerException
  • ClassNotFoundException/NoClassDefFoundError
  • IllegalStateException
  • IllegalArgumentException
  • IllegalAccessException
  • ClassCastException
  • ...

五、异常的处理实战

1. 找到第一个包含text的行的行号

行号从1开始计算。若没找到,则返回-1。
如果指定的文件不存在或者无法被读取,抛出一个IllegalArgumentException,请不要让这个方法抛出checked exception:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class FileSearch {
    // 找到第一个包含text的行的行号,行号从1开始计算。若没找到,则返回-1。
    // 如果指定的文件不存在或者无法被读取,抛出一个IllegalArgumentException。
    // 请不要让这个方法抛出checked exception
    private static final int TEXT_NOT_FOUND = -1;

    public static int grep(File target, String text) {
        try (BufferedReader br = new BufferedReader(new FileReader(target))) {
            String line;
            int lineNum = 1;
            for (line = br.readLine(); line != null; lineNum++, line = br.readLine()) {
                if (line.contains(text)) {
                    return lineNum;
                }
            }
            return TEXT_NOT_FOUND;
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static void main(String[] args) {
        File projectDir = new File(System.getProperty("basedir", System.getProperty("user.dir")));
        System.out.println("结果行号:" + grep(new File(projectDir, "log.txt"), "BBB"));
    }
}

你可能感兴趣的:(中级06 - Java的异常体系)