# Throwable

  • Java 把所有的非正常情况分成两种:异常(Exception)和错误(Error),它们都继承 Throwable 父类

  • 构造器
    Throwable()、Throwable(String message)、Throwable(Throwable cause)
    Throwable(String message, Throwable cause):构造一个带指定详细消息(以后通过 getMessage() 方法获取)和原因(以后通过 getCause() 方法获取)的新 throwable

  • 实例方法

    1. String getMessage():返回该异常的描述信息(提示给用户)
    2. String toString():返回该异常的类型和描述信息(不用)
    3. void printStackTrace():打印异常的跟踪栈信息到控制台,包括异常的类型、异常的原因、异常出现的位置(开发和调试)

# 异常分类

异常体系

  1. 编译时异常(Checked 异常):除了 RuntimeException 及其子类以外,其它的 Exception 及其子类
    • 编译器要求必须处理的异常,因此程序中一旦出现这类异常,必须显式处理(捕获或抛出),否则编译无法通过
    • 常见的编译时异常:ParseException、InterruptedException、IOException(子类:FileNotFoundException)、ClassNotFoundException、SQLException
  2. 运行时异常(Runtime 异常 / UnChecked 异常):RuntimeException 及其子类
    • 编译器不要求强制处理的异常,程序中出现这类异常时,可以不处理
    • 常见的运行时异常:ArithmeticException、IndexOutOfBoundsException(子类:ArrayIndexOutOfBoundsException、StringIndexOutOfBoundsException)、NullPointerException、ClassCastException、IllegalArgumentException(子类:NumberFormatException、IllegalThreadStateException)、IllegalStateException、IllegalMonitorStateException、UnsupportedOperationException、NoSuchElementException
    • Java 类库中定义的运行时异常类应由程序员预检查来规避,而不是捕获

# 使用 try ... catch 捕获异常

  • 语法结构

    try {
        // 可能会出现异常的代码
    } catch (要捕获的异常类型A e) {
        // 处理异常的代码:记录日志/打印异常信息/继续抛出异常等
    } catch (要捕获的异常类型B e) {
        // 处理异常的代码:记录日志/打印异常信息/继续抛出异常等
    } finally {
        // 关闭资源对象、流对象等
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 当程序运行出现异常,系统会自动生成一个异常对象,该异常对象被提交给 Java 运行时环境,这个过程被称为抛出(throw)异常

  • 如果执行 try 块里的代码时出现异常,Java 运行时环境收到异常对象后,会寻找能处理该异常对象的 catch 块,如果找到合适的 catch 块,则把该异常对象交给该 catch 块处理,这个过程被称为捕获(catch)异常

  • 如果 Java 运行时环境找不到捕获该异常的 catch 块,运行时环境终止该线程也将退出

  • 在处理多种异常类型时,必须先捕获子类类型异常,后捕获父类类型异常,否则编译报错(最后捕获 Exception 类型异常,确保异常对象能被捕获到)

  • 不管 try 块中的代码是否出现异常,也不管哪一个 catch 块被执行,甚至在 try 块或 catch 块中执行了 return 语句,finally 块总会被执行(除非在 try 块或会执行的 catch 块中调用退出 JVM 的相关方法)

  • try 块必须和 catch 块或和 finally 块同在,不能单独存在,catch 块或和 finally 块二者必须出现一个,finally 块必须位于所有的 catch 块之后

  • 当程序执行 try 块、catch 块时遇到 return 或 throw 语句时,系统不会立即结束该方法,而是去寻找该异常处理流程中是否包含 finally 块,如果有 finally 块,系统立即开始执行 finally 块——只有当 finally 块执行完成后,系统才会再次跳回来执行 try 块、catch 块里的 return 或 throw 语句;如果 finally 块里也使用了 return 或 throw 等导致方法终止的语句,finally 块已经终止了方法,系统将不会跳回去执行 try 块、catch 块里的任何代码

  • JVM 通过异常表(Exception table)来完成 catch 语句

注意确保 finally 代码块不出现异常,内部把异常处理完毕,避免 finally 中的异常覆盖 try 中的异常;或者考虑使用 addSuppressed 方法把 finally 中的异常附加到 try 中的异常上,确保主异常信息不丢失

子线程抛出异常堆栈,不能在主线程 try-catch 到

对于线程中未捕获到异常默认处理方式:JVM 调用 Thread#dispatchUncaughtException 方法 ==> 如果该线程没有定义 uncaughtExceptionHandler,再调用 ThreadGroup#uncaughtException 方法 ==> 如果没有定义全局默认的 defaultUncaughtExceptionHandler,则 e.printStackTrace(System.err),即向标准错误输出打印出现异常的线程名称和异常信息

public void test() throws Exception {
    Exception e = null;
    try {
        log.info("try");
        throw new RuntimeException("try");
    } catch (Exception ex) {
        e = ex;
    } finally {
        log.info("finally");
        try {
            throw new RuntimeException("finally");
        } catch (Exception ex) {
            if (e!= null) {
                e.addSuppressed(ex);
            } else {
                e = ex;
            }
        }
    }
    throw e;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ThreadGroup 的相关源码
public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        if (ueh != null) {
            ueh.uncaughtException(t, e);
        } else if (!(e instanceof ThreadDeath)) {
            System.err.print("Exception in thread \"" + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}

// 处理方式:
// 1. 设置全局的默认未捕获异常处理程序
// 2. 设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 使用 throws 声明抛出异常

  • 语法格式

    [修饰符] 返回值类型 方法名(参数列表) throws 异常类A, 异常类B, ...{
    }
    
    1
    2
  • 在可能出现异常的方法上声明可能抛出的异常类型,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常

  • 当前方法使用 throws 声明抛出异常,该异常将交给上一级调用者处理,调用者要么 try ... catch,要么也 throws

  • 如果 main 方法也使用 throws 声明抛出异常,该异常将交给 JVM 处理,JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行

  • 子类方法声明抛出的异常类和父类方法声明抛出的异常类相同或者是其子类(运行时异常除外)

# 使用 throw 自行抛出异常

  • 语法格式

    throw new 异常类("异常信息"); // 终止方法
    
    1
  • throw 语句可以单独使用,throw 后面只能跟一个异常对象

  • 有返回值的方法中,可以使用 throw 来避免返回一个空值

  • return 或 throw 语句到所在结束的花括号之间不能有其它的语句,否则编译报错

  • 如果 throw 语句抛出的异常是 Checked 异常,则该 throw 语句必须处于 try 块里,或处于带 throws 声明的方法中

  • 如果 throw 语句抛出的异常是 Runtime 异常,则该语句无须放在 try 块里,也无须放在带 throws 声明抛出的方法中

  • 在 catch 块中使用 throw 语句,方法既可以捕获异常,还可以抛出异常给方法的调用者

# 自定义异常类型

  • 自定义 Checked 异常,应继承 Exception
  • 自定义 Runtime 异常,应继承 RuntimeException(推荐)
// 自定义业务逻辑异常
public class LogicException extends RuntimeException {
    // 无参数的构造器
    public LogicException() {
        super();
    }
    // 带一个字符串参数的构造器
    public LogicException(String message) {
        super(message);
    }
    // message 当前异常的原因/信息;cause 当前异常的根本原因
    public LogicException(String message, Throwable cause) {
        super(message, cause);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 异常转译和异常链

  • 异常转译:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息

  • 异常链:捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来

// 工资计算方法
void calSal() throws SalException {
    try {
        ... // 业务逻辑
    } catch (SQLException sqle) {
        ... // 把原始异常记录下来,留给管理员
        // 下面异常中的 message 就是对用户的提示
        throw new SalException("访问底层数据库出现异常");
    } catch (Exception e) {
        ... // 把原始异常记录下来,留给管理员
        // 下面异常中的 message 就是对用户的提示
        throw new SalException("系统出现未知异常");
    }
}

// 定义 SalException 异常类
public class SalException extends Exception {
    public SalException() {
    }
    
    public SalException(String msg) {
        super(msg);
    }
    
    // 创建一个可以接受 Throwable 参数的构造器
    public SalException(Throwable t) {
        super(t);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# Java 7 的异常新特性

  • 增强的 throw 语句
    编译器会检查 throw 语句抛出异常的实际类型,因此在方法签名中可以声明抛出该异常对象的实际类型异常

  • 多异常捕获
    使用一个 catch 块捕获多种类型的异常:多种异常类型之间用竖线(|) 隔开,异常变量有隐式的 final 修饰,不能被重新赋值

  • 自动关闭资源的 try 语句(try-with-resources)
    允许在 try 关键字后紧跟一对圆括号,圆括号内可以声明、初始化实现了 AutoCloseable 或 Closeable 接口的资源实现类对象,try 语句在该语句结束时自动关闭这些资源

    try (
            // 声明、初始化两个可关闭的资源
            // try 语句会自动关闭这两个资源
            BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
            PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))) {
        // 使用两个资源
        System.out.println(br.readLine());
        ps.println("try-with-resources");
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 异常处理规则

  • 不要过度使用异常:
    • 对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性
    • 对外部的、不能确定和预知的运行时错误才使用异常
  • 不要使用过于庞大的 try 块
  • 避免使用 catch all 语句
  • 不要忽略捕获到的异常
Updated at: 2022-12-26 15:35:28