异常处理机制
# 机制概述
在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。
抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着 Java 程序的终止。
由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java 规定,运行时异常将由 Java 运行时系统自动抛出,允许应用程序忽略运行时异常。
对于方法运行中可能出现的 Error,当运行方法不欲捕捉时,Java 允许该方法不做任何抛出声明。因为,大多数 Error 异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
对于所有的可查异常,一个方法必须捕捉,或者声明抛出方法之外,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。
能够捕捉异常的方法,需要提供相符类型的异常处理器,设计逻辑如下图:
从方法中抛出的任何异常都必须使用 throws 子句,捕捉异常通过 try-catch 语句或者 try-catch-finally 语句实现。
# 异常处理设计
# 抛出异常
任何 Java 代码都可以抛出异常,如:自己编写的代码、来自 Java 开发环境包中代码,或者 Java 运行时系统。无论是哪一种情况,都可以通过 Java 的 throw 语句抛出异常。从方法中抛出的任何异常都必须使用 throws 子句。throws 语句用在方法定义时声明该方法要抛出的异常类型,如果抛出的是 Exception 异常类型,则该方法被声明为抛出所有的异常。多个异常可使用逗号分割。
方法名后的throws Exception1,Exception2,...,ExceptionN
为声明要抛出的异常列表。当方法抛出异常列表的异常时,方法将不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法去处理。使用 throws 关键字将异常抛给调用者后,可以继续向上抛出,但最终要有能够处理该异常的调用者。
Throws 抛出异常的规则:
- 如果是不可查异常(
unchecked exception
),即Error
、RuntimeException
或它们的子类,那么可以不使用 throws 关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。 - 必须声明方法可抛出的任何可查异常(
checked exception
)。即如果一个方法可能出现受可查异常,要么用 try-catch 语句捕获,要么用 throws 子句声明将它抛出,否则会导致编译错误。 - 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
- 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
# 捕获异常
在 Java 中,异常通过 try-catch 语句捕获。关键词 try 后的大括号将可能发生异常的代码包起来,称为监控区域。Java 方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,由 Java 运行时系统试图寻找匹配的 catch 子句以捕获异常。若有匹配的 catch 子句,则运行其异常处理代码,try-catch 语句结束。匹配的原则是:如果抛出的异常对象属于 catch 子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与 catch 块捕获的异常类型相匹配。
try-catch 语句还可以包括第三部分,就是 finally 子句。它表示无论是否出现异常,都应当执行的内容。
异常处理语句的语法规则:
- 必须在 try 之后添加 catch 或 finally 块。try 块后可同时接 catch 和 finally 块,但至少有一个块。
- 必须遵循块顺序:若代码同时使用 catch 和 finally 块,则必须将 catch 块放在 try 块之后。
- catch 块与相应的异常类的类型相关。
- 一个 try 块可能有多个 catch 块。若如此,则执行第一个匹配块。即 Java 虚拟机会把实际抛出的异常对象依次和各个 catch 代码块声明的异常类型匹配,如果异常对象为某个异常类型或其子类的实例,就执行这个 catch 代码块,不会再执行其他的 catch 代码块。
- 可嵌套 try-catch-finally 结构。
- 在 try-catch-finally 结构中,可重新抛出异常。
- 除了下列情况,总将执行 finally 做为结束:JVM 过早终止(调用
System.exit(int)
);在 finally 块中抛出一个未处理的异常;计算机断电、失火、或遭遇病毒攻击。
语句块的执行顺序:
- 当 try 没有捕获到异常时:try 语句块中的语句逐一被执行,程序将跳过 catch 语句块,执行 finally 语句块和其后的语句;
- 当 try 捕获到异常,catch 语句块里没有处理此异常的情况:当 try 语句块里的某条语句出现异常时,而没有处理此异常的 catch 语句块时,此异常将会抛给 JVM 处理,finally 语句块里的语句还是会被执行,但 finally 语句块后的语句不会被执行;
- 当 try 捕获到异常,catch 语句块里有处理此异常的情况:在 try 语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到 catch 语句块,并与 catch 语句块逐一匹配,找到与之对应的处理程序,其他的 catch 语句块将不会被执行,而 try 语句块中,出现异常之后的语句也不会被执行,catch 语句块执行完后,执行 finally 语句块里的语句,最后执行 finally 语句块后的语句。
# Java 常见异常
类型 | 异常 | 说明 |
---|---|---|
RuntimeException | java.lang.ArrayIndexOutOfBoundsException | 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。 |
java.lang.ArithmeticException | 算术条件异常。譬如:整数除零等。 | |
java.lang.NullPointerException | 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等 | |
java.lang.ClassNotFoundException | 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。 | |
java.lang.NegativeArraySizeException | 数组长度为负异常 | |
java.lang.ArrayStoreException | 数组中包含不兼容的值抛出的异常 | |
java.lang.SecurityException | 安全性异常 | |
java.lang.IllegalArgumentException | 非法参数异常 | |
IOException | IOException | 操作输入流和输出流时可能出现的异常。 |
EOFException | 文件已结束异常 | |
FileNotFoundException | 文件未找到异常 | |
其他 | ClassCastException | 类型转换异常类 |
ArrayStoreException | 数组中包含不兼容的值抛出的异常 | |
SQLException | 操作数据库异常类 | |
NoSuchFieldException | 字段未找到异常 | |
NoSuchMethodException | 方法未找到抛出的异常 | |
NumberFormatException | 字符串转换为数字抛出的异常 | |
StringIndexOutOfBoundsException | 字符串索引超出范围抛出的异常 | |
IllegalAccessException | 不允许访问某类异常 | |
InstantiationException | 当应用程序试图使用Class类中的newInstance()方法创建一个类的实例,而指定的类对象无法被实例化时,抛出该异常 |
# 自定义异常
用户自定义异常类,只需继承 Exception 类即可。在程序中使用自定义异常类,可分为以下几个步骤:
- 创建自定义异常类。
- 在方法中通过 throw 关键字抛出异常对象。
- 如果在当前抛出异常的方法中处理异常,可以使用 try-catch 语句捕获并处理;否则在方法的声明处通过 throws 关键字指明要抛出给方法调用者的异常,继续进行下一步操作。
- 在出现异常方法的调用者中捕获并处理异常。