认识异常
程序中可能会有很多意想不到的问题的出现,这些问题中,有些是在编写阶段时就无法编译通过,比如写代码时变量名写错,出现语法错误 java.lang.Error: Unresolved compilation problem ……;有些是在程序运行的时候出现的,比如一个除法程序,结果用户输入的除数是 0,那么就会引发 java.lang.ArithmeticException 等等。异常发生的原因很多,但无论怎样,这些异常和其他的对象一样,都只是类的实例。
异常分类
Java 中异常处理主要有三类:
- 检查性异常(Checked Exception):检查一词可以理解为编译器对代码的检查,如果代码中含有此类异常且没有进行处理,编译就无法通过,比如文件无法找到的异常等。这种异常可以说是 Java 的特色,其他流行语言少有这种异常。这样的异常一般是由程序运行的环境所导致的,程序员无法知道程序最终的运行环境,所以应该时刻准备好应对这些异常(用 try ... catch ... 捕获并处理);
- 运行时异常(Runtime Exception):这是编译器无法检查到,程序运行时才会出现的异常,比如除数为零这样的异常。对于这类异常,程序员应该先尝试直接修改代码的逻辑避免出现此类异常,实在不行时再用 try ... catch ... 去捕获它并进行处理。一个好的代码,应该尽可能少地抛出运行时异常;
- 错误(Error):错误是 Java 程序无法处理的严重故障,程序员一般不应该去捕获它们,捕获了可能也无法处理,这也是程序员无法预见的,在编译时编译器也无法检查到,大部分错误都与程序员执行的操作无关,而是虚拟机本身在平台上的运行出现了问题。
下面是异常机制的继承关系图:
内置异常
Java 语言内置的标准异常类型都在 java.lang 标准包中。
运行时异常
下面只列出部分常用的运行时异常:
异常类型名 | 描述 |
ArithmeticException | 当出现异常的运算条件时(如除数为零),抛出此异常 |
ArrayIndexOutOfBoundsException | 用非法索引(索引为负或大于等于数组大小)访问数组时,抛出此异常 |
ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时,抛出此异常 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出此异常 |
IllegalArgumentException | 向方法传递了一个不合法或不正确的参数时,抛出此异常 |
IllegalMonitorStateException | 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程 |
IllegalStateException | 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下 |
IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常 |
IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出 |
NegativeArraySizeException | 如果应用程序试图创建大小为负的数组,则抛出该异常 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常 |
SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯 |
StringIndexOutOfBoundsException | 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小 |
UnsupportedOperationException | 当不支持请求的操作时,抛出此异常 |
检查性异常
下面只列出部分检查性异常:
异常类型名 | 描述 |
ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常 |
CloneNotSupportedException | 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常 |
IllegalAccessException | 拒绝访问一个类的时候,抛出该异常 |
InstantiationException | 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常 |
InterruptedException | 一个线程被另一个线程中断,抛出该异常 |
NoSuchFieldException | 请求的变量不存在 |
NoSuchMethodException | 请求的方法不存在 |
错误
下面只列出部分错误类型:
错误类型名 | 描述 |
LinkageError | 动态链接失败 |
VirtualMachineError | 虚拟机错误 |
AWTError | AWT错误 |
异常的方法
这里只说明 Throwable 的方法,其子类异常还有其他自己的方法,这里不展开讲述。下面是 Throwable 类的主要方法:
方法名 | 描述 |
public String getMessage() | 返回关于发生的异常的详细信息 |
public Throwable getCause() | 返回一个 Throwable 对象代表异常原因 |
public String toString() | 返回此 Throwable 的简短描述 |
public void printStackTrace() | 将此 Throwable 及其回溯打印到标准错误流 |
public StackTraceElement [] getStackTrace() | 返回一个包含堆栈层次的数组,下标为 0 的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底 |
public Throwable fillInStackTrace() | 用当前的调用栈层次填充 Throwable 对象栈层次,添加到栈层次任何先前信息中 |
捕获异常
try ... catch ... 语句
和 C++ 语言类似,使用 try 关键字和 catch 关键字捕获异常。在 try 语句块中的部分代码是可能出现异常的代码,catch 关键字后面表示捕获哪一种特殊的异常,并在其语句块中进行相应的处理。
try {
// 某种异常
} catch (ExceptionType e) { // ExceptionType表示某种异常类型,e是这个异常实例对象的引用名
// 处理方式
}
下面是一个使用 try ... catch ... 的示例:
public class Test {
public static void main(String[] args) {
System.out.println(divide(1, 0));
// Output:
// 除数不能为零!
// 0
}
public static int divide(int num1, int num2) {
try {
return num1 / num2;
} catch (ArithmeticException e) {
System.out.println("除数不能为零!");
return 0;
}
}
}
多重捕获
当异常类型不只一种的时候,就需要多重捕获。
try {
// 某种异常
} catch (ExceptionType1 e1) {
// 对于第一种异常的处理方式
} catch (ExceptionType2 e2) {
// 对于第二种异常的处理方式
}
finally 关键字
这个 finally 关键字和 Python 异常处理中的 finally 关键字类似,都是在 try ... catch ... 语句执行完后执行 finally 关键字中的内容。
try {
// 捕获异常
} catch (ExceptionType e) {
// 处理异常
} finally {
// 最后执行(不管上面是否对异常进行了处理)
}
下面和 Python 的异常处理进行对比:
""" Python 的异常处理 """
try:
... # 捕获异常
except ExceptionType as e: # e是异常类型的实例对象的引用变量名
... # 处理异常
else:
... # 没有异常时执行的代码
finally:
... # 最后必定执行的代码
除了 else 是 Python 中特有的用法之外,其余部分和 Java 中的几乎完全相同。另外,标准 C++ 的异常处理中没有 finally 关键字(MSVC 中有个 __finally 关键字做了 finally 的替代,但其他的 C++ 编译器不一定有)
总结一下,下面是 try ... catch ... finally ... 语句的执行流程图:
此外,还要注意以下几点:
- catch 或者 finally 关键字不能独立于 try 关键字而存在;
- 当不是 try-with-resources 语法时,try 后面必须接上 catch 或者 finally 关键字;
- 多重捕获时,具体异常要放在宽泛异常之前,不然无法捕获到(逻辑上宽泛异常包含了具体异常);
try-with-resources 语法糖
try-with-resouces 语法糖是 Java 的一种特殊语法,是在 Java7 为了简化语法(方便打开资源)而引入的。在 Java7 之前,打开文件、套接字等,都需要程序员手动地去关闭这些资源,很麻烦,用 finally 也不方便。于是就在 Java7 出现了这个语法糖,它可以在执行完相关的操作后自动关闭这些资源。
该语法糖的基本格式如下:
try (resource_declaration_1; resource_declaration_2; ...) { // 圆括号内可声明或实例化一个或多个(用分号间隔)资源对象
// 使用的资源
} // 执行完后,资源对象将被自动关闭,关闭顺序与声明或实例化顺序相反
下面是一个示例:
public static void try_with_res() throws IOException { // 下面产生的异常(被声明了)会传递到方法外部,相当于处理了(后面会讲)
try (Scanner s = new Scanner(new File("input.txt")); PrintWriter w = new PrintWriter(new File("output.txt"))) {
while (s.hasNext())
w.print(s.nextLine());
}
}
其实这个语法糖在资源打开操作方面和 Python 中的 with 关键字有异曲同工之妙,都可以简化资源的操作(打开后自动关闭),下面给出一个示例(Python3.8)以进行对比:
with open('input.txt', 'r') as input_file, open('output.txt', 'w') as output_file:
while line := input_file.readline():
output_file.write(line)
抛出异常和声明异常
throw 关键字
程序员可以用 throw 关键字来显式地抛出异常。
throw new ExceptionObject(...);
下面是一个示例:
public static void CheckArgument(Object arg) {
if (arg instanceof Integer) {
throw new IllegalArgumentException("参数类型不能为整型!");
}
}
throws 关键字
程序员可以用 throws 关键字来为方法声明可能抛出的异常,这只是一个声明,并非运行时一定抛出该异常。当该方法中真的抛出声明中指定的异常时,此异常就会向外传递到方法外部,也就是调用该方法的地方。throws 关键字放在方法头之后,花括号之前,后面跟要声明的异常,可以有多个,通过逗号间隔。
下面是一个示例:
public static void CheckArgument(Object arg) throws IllegalArgumentException, UnknownError {
if (arg instanceof Integer) {
throw new IllegalArgumentException("参数类型不能为整型!");
}
}
自定义异常
程序员除了可以使用 Java 内置的异常之外,还可以自己定义一个异常来使用,但无论怎样,自定义的异常也是异常最顶层父类 Throwable 的子类。用途的不同的异常,需要继承的父类异常也不一样,若是要自定义运行时异常,则应该继承类 RuntimeException;若是检查性异常,则应该继承类 Exception;若是错误,则应该继承类 Error。
自定义的异常类型和其他的异常一样,实际都是对象。自定义的异常一般应该满足以下条件,当然,这不是强制的:
- 有一个无参构造函数;
- 有一个带有 String 参数的构造函数,并传递给父类的构造函数;
- 有一个带有 Throwable 参数的构造函数,并传递给父类的构造函数;
- 有一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数;
下面是一个自定义异常的简单示例:
class ZeroDivisionException extends ArithmeticException {
}