【Java SE】十二、异常处理

Java 中的所有异常都被封装成一个类且都继承于 Exception 类,而它是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类 Error,但 Java 程序通常不捕获错误,它们一般在 Java 程序处理的范畴之外,所以只是用来指示运行时环境发生的错误。

tree

要理解 Java 异常处理是如何工作的,你需要掌握以下三种类型的异常:

  • **检查性异常:**最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

捕获异常

基本操作

Java 中使用 trycatch 关键字可以捕获异常。try-catch 代码块被放在异常可能发生的地方。

// 下面的例子中声明有两个元素的一个数组,当代码试图访问数组的第四个元素的时候就会抛出一个异常
import java.io.*;
public class ExcepTest {
    public static void main(String args[]){
        try {
            int a[] = new int[2];
            System.out.println("Access element three :" + a[3]);
        } catch(ArrayIndexOutOfBoundsException e) { // 声明捕获数组下标越界异常类的引用变量e作为形参
            System.out.println("Exception thrown  :" + e); // 此处e则表示ArrayIndexOutOfBoundsException类的实例
        }
        System.out.println("Out of the block");
    }
}

catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查,如果发生的异常包含catch 块中,异常会被传递到该 catch 块的形参 e 中,这和传递一个参数到方法是一样。

多重捕获块

一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。

多重捕获块的语法如下所示:

// 下面的代码段包含了 2 个 catch 块,可以在 try 语句后面添加任意数量的 catch 块
try {
    file = new FileInputStream(fileName);
    x = (byte) file.read();
} catch(FileNotFoundException f) { // 不匹配,跳过
    f.printStackTrace();
    return -1;
} catch(IOException i) { // 匹配,被捕获,执行代码块
    i.printStackTrace();
    return -1;
}

如果保护代码中发生异常,异常会按照先后顺序一层一层的匹配 catch 块中的异常,直到异常被捕获或者通过所有的 catch 块。

为了避免异常通过所有的 catch 块,我们一般会将异常类按辈分从小到大的顺序自上而下来声明 catch 块,如下所示:

try{
    // Code
} catch(FileNotFoundException e) { // 最小
    // Code
} catch(IOException e) { // 中间
    // Code
} catch(Exception e) { // 所有异常的父类,绝对漏不掉
    // Code
}

finally 关键字

finally 关键字用来创建在 try 代码块后面执行的代码块。无论是否发生异常,finally 代码块中的代码总会被执行,所以常用来运行清理类型等收尾善后性质的语句。

public class ExcepTest {
    public static void main(String args[]) {
        int a[] = new int[2];
        try {
            System.out.println("Access element three :" + a[3]);
        } catch(ArrayIndexOutOfBoundsException e) { // 捕获异常,执行代码块
            System.out.println("Exception thrown  :" + e);
        } finally { // 无论有没有异常,都会执行代码块
            a[0] = 6;
            System.out.println("First element value: " +a[0]);
            System.out.println("The finally statement is executed");
        }
    }
}

运行结果如下:

Exception thrown  :java.lang.ArrayIndexOutOfBoundsException: 3
First element value: 6
The finally statement is executed

注意事项:

  • catch 不能独立于 try 存在
  • try 代码后不能既没 catch 块也没 finally
  • try, catch, finally 块之间不能添加任何代码

抛出异常

如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。

无论是新实例化的还是刚捕获到的,也可以使用 throw 关键字抛出一个异常,将其交给调用者去处理。

import java.io.*;
public class className { // 一个方法可以声明抛出多个异常,多个异常之间用逗号隔开
    public void withdraw(double amount) throws RemoteException,InsufficientFundsException {
        // Method implementation
        throw new RemoteException("Error"); // 手动抛出异常,注意用的是throw关键字
    }
    // Remainder of class definition
}

注意事项:

  • 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型
  • 当方法体内部的异常符合声明抛出的异常,则会被抛出,异常后续代码不被执行
  • throws 的方式只是将异常抛给了方法的调用者。并没有真正将异常处理掉
  • 手动抛出的异常是由自己手动生成并抛出的,并非系统运行生成的

自定义异常

在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。

  • 必须继承现有的异常结构,如 Exception、RuntimeException 等
  • 可以提供全局常量:serialVersionUID,用来作为该异常的标识
  • 提供重载的构造器
  • 如果希望写一个检查性异常类,则需要继承 Exception 类。
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。

可以像下面这样定义自己的异常类:

pub1ic class MyException extends RuntimeException {
    
    static final long serialVersionUID = -7034897193246939L; // 标识符,可选
    
    public MyException(){} // 空参构造器
    public MyException(String msg) { // 直接调用父类现成的有参构造器
    	super(msg);
    }
}