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 错误
- Exception - 受检异常,预料之中
在 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"));
}
}