分类

  1. Error : 系统错误,大多数错误与代码编写者无关。一般指 JVM 内部出现的问题。例如:JVM没有足够的内存提供给垃圾回收器,堆栈溢出等。此类错误发生时,JVM 将终止线程。编译器不会辅助检查。
  2. Exception : 与程序有关的异常。
    1. RuntimeException : 程序逻辑错误引起的。编译器不会辅助检查(可以通过编译)
      • 是编程人员的问题
      • 4/0,空指针, 数组越界, 非法参数 ······ 我们应该从逻辑上去解决并改进代码。
      • 在程序中可以选择捕获、抛出,也可以不处理。
      • 就算没有异常捕获语句,异常也会由 JVM 抛出。
    2. IOException : 称为其他异常。由程序外部引起的错误。编译器会辅助检查。
      • 也就是说当编译器检查到可能会有异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过
      • 如:打开一个不存在的文件,加载一个不存在的类 ······

非检查型异常(Unchecked Exception):RuntimeException 和 Error。不用声明 Error 异常,因为任何代码都有可能抛出这些异常。也不应该声明从 RuntimeException 继承的那些异常。

检查型异常(Checked Exception):IOException(所有其他异常)。检查型异常首部要指出这个方法可能抛出一个异常,什么时候需要在方法中用 throws 子句声明异常

  1. 调用了一个抛出检查型异常的方法
  2. 检测到一个错误,并利用 throw 语句抛出一个检查型异常
  3. 程序出现错误,如 a [ -1 ]。(RuntimeException)
  4. Java 虚拟机或运行时库出现内部错误。(Error)

如果是前两种情况,必须告诉调用这个方法的程序员有可能抛出异常。为什么?因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前执行的线程就会终止。

子类方法中声明的检查型异常不能比超类方法中声明的检查型异常更通用。

如何抛出异常

throw new EOFException();
// 或者
var e = new EOFException();
throw e;

一旦方法抛出了异常,就不会返回到调用者。

String readData(Scanner in) throws EOFException {
    if (!in.hasNext()) {
        throw new EOFException();
    }
    ...
    return s
}

自定义异常

代码可能遇到任何标准异常都无法描述清楚的问题,就可以创建自己的异常类。

我们只要定义一个派生于 Exception 的类,或 Exception 的某个子类。

习惯上,自定义的类包含两个构造器,一个是默认的构造器,另一个是带有详细描述信息的构造器(超类 Throwable 的 toString 方法会返回一个字符串,其中包含这个详细信息,在调试中很有用。)。如:

class FileFormatException extends IOException {
    public FileFormatException() {}
    public FileFormatException(String g) {
        super(g); // == super(g.toString());
    }
}

捕获异常

处理异常:用 try / catch 语句块

  • 如果 try 没有抛异常,将跳过 catch 子句。
  • 如果抛异常了并能被 catch 捕获,将跳过 try 子句中的其余代码,执行 catch 子句中的处理器代码。
  • 如果抛出的异常 catch 无法捕获,则会立刻退出这个方法。

传递异常:不过最简单的办法是直接不使用 try / catch 语句块,而是抛出异常(throws Exception)。

上面两种方法都可以,如果调用了一个抛出检查型异常的方法,就必须处理这个异常,或者继续传递这个异常。

思考:捕获一个异常,还是抛出异常让别人捕获?

捕获那些你知道如何处理的异常,继续传播自己不知道怎么处理的异常。

再次抛出异常与异常链

可以在 catch 子句中抛出一个异常,通常,希望改变异常的类型时会这样做。如:

try {
} catch(SQLException e) {
    throw new ServletException(e.getMessage);
}
// 如:我们可能不想知道原始异常是什么,只是想知道 servlet 是否有问题

下面提供了一种更好的解决方案:把原始异常设为新异常的原因

try {
} catch(SQLException original) {
    var e new ServletException("data error");
    e.initCause(original);
    throw e; // 抛出 ServletException 异常
}
// 捕获到这个异常时,可以使用下面这条语句获取原始异常
try {
} catch(ServletException s) {
    s.getCause(); // 获取原始异常
}

建议使用这种包装技术, 这样可以在子系统中抛出高层异常, 而不会丢失原始异常的细节。

如果在一个方法中发生了一个检查型异常,但这个方法不允许抛出检查型异常,我们可以用包装技术:捕获这个检查型异常,包装为一个运行时异常。

try-catch-finally

  • 必需有try
  • catch和finally至少有一个
  • finally一定会被执行
  • try,catch,finally每个模块都可以继续写try-catch-finally语句
  • catch 里面的小异常写在上面,大异常写下面

当 finally 子句中包含 return 语句时:假如 try 中因为 return 语句退出,但是在方法返回前会执行 finally 子句,如果 finally 子句中也有 return 语句,它会覆盖 try 的 return 语句。

更糟糕的是:如果 try 中的 return 语句抛出了异常,如果 finally 子句中的 return 语句可能会吞掉这个异常。

finally 中要进行资源清理,不要把控制流的语句放在 finally 中(return , throw , break , continue)

try-with-resource

try (var in = new Scanner(...)) {
    while (in.hasNext()) System.out.println(in.next());
}
// 这个块正常退出时,或者存在一个异常时,都会调用 in.close() 方法,就好像使用了 finally 一样。
// 从这个名字 try-with-resource ,我们也可以猜到,它是用来操作资源相关的,如果要关闭资源,尽量用 try-with-resource

异常处理

  • 方法可能存在异常的语句,但不处理,那么可以使用throws来声明异常
  • 调用带有throws异常的方法,要么处理这些异常,或者再次向外throws,直到main函数为止。
  • 在方法内部抛出异常关键字为throw
  • 子类不能比父类抛出更多的异常