异常处理
一、异常类
Java中提供了众多的异常类,各种异常类之间的层次关系如图所示:
图中最上位的Throwable类为Object类的子类,另外,Throwable、Error、Exception都属于java.lang包。
Throwable 类
Throwable位于异常类的层次结构的顶端。也就是说,Java中所有的异常类都是它的下位类。因此存在如下规则:
1)当声明catch子句中的形参时.如果指定的类型不是Throwable的下位类,就会发生编译错误;
2)当自己创建异常类时、必须将其创建为Throwable的下位类Throwable的子类为Error类和Exception类。
Error 类
这是程序没有希望(无法)恢复的重大异常。正如其名称所示,与其说是“异常",倒不如说是“错误” 更为准确;
通常情况下,程序中无需对此类进行捕获、处理,因为即使捕获了,也难以甚至无法处理。
Exception 类
这是程序有希望(可以)恢复的异常。如图所示,该类的直接下位类中包含RuntimeException类,Exception类的下位类基本上都被称为检查异常(checked exception)的异常。不过,RuntimeException类及其下位类为非检查异常
检查异常和非检查异常
异常分为两种,它们的区别很大:
1)检查异常
检查异常是必须处理的异常,编译时会检查程序中是否对其进行了处理, 对于此类异常,必须进行捕获和处理,如果下述两项中有一项未执行 ,就会发生编译错误
1)将可能会抛出检查异常的代码放到try语句中, 以捕获该异常;
2)将方法和构造函数的声明中可能会抛出的异常明确记述到throws子句中;
2)非检查异常
非检查异常是并非一定要处理的异常 程序中可以对其进行处理, 也可以不对其进行处理, 编译时不会检查是否进行了处理 即使不对其进行捕获和处理. 也不会发生编译错误;
1)可以不将可能会抛出非检查异常的代码放到try语句中;
2)对于可能会抛出非检查异常的方法和构造函数, 无需将这些异常明确记述到throws子句中
二、Throwable类
Throwable类是所有异常类的 “老大类" ,在理解异常处理的相关内容时必须要充分理解该类
构造函数
Throwable的构造函数的概要如表所示:
上表中可知,可以设置详细消息和原因:
所谓原因, 就是制造该异常发生的契机的异常。如果以发生了异常A为契机, 而发生了异常B的话,当构建异常B的实例时, 就可以将A设置为原因
异常主体
Java的异常中至少包含详细消息和原因两种信息, 是Throwable类的下位类类型的实例,如果发生异常, 那么持有相关信息的实例就会被创建,异常的主体是Throwable类的下位类的实例, 包含详细消息和异常发生的原因等信息。
方法
消息和原因等信息可以从异常实例中取出, 下表中汇总了用于实现此操作的方法。
最后6个是与栈跟踪相关的方法,不仅可以将栈跟踪输出到画面上, 还可以将其分解取出
Exception类和RuntimeException类
Throwable类的直接下位类Exception类和RuntirneException类中也定义了与Throwable形式相同(接收相同参数)的构造函数,而且它们也直接继承了上表所示的主要方法。
三、抛出和捕获异常
package com.example;// 用于理解异常处理的示例
import java.util.Scanner;
class ThrowAndCatch {
//--- 发生sw值所对应的异常 ---//
static void check(int sw) throws Exception {
switch (sw) {
case 1: throw new Exception("发生检查异常!!");
case 2: throw new RuntimeException("发生非检查异常!!");
}
}
//--- 调用check ---//
static void test(int sw) throws Exception {
check(sw);
}
public static void main(String[] args) {
Scanner stdIn = new Scanner(System.in);
System.out.print("sw:");
int sw = stdIn.nextInt();
try {
test(sw);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
输出:
check方法
本方法根据参数SW中接收到的值,抛出Exception或者RuntimeException异常throws子句(声明可能抛出的检查异常)
throws Exception部分的方法声明就是throws子句,可能抛出检查异常的方法会将所有异常都列举到throws子句中(有多个异常时,使用逗号隔开);
方法的抛出
在方法主体的switch语句中,会抛出SW值所对应的异常throw语句用于抛出异常,其形式为"throw表达式;";
指定的表达式为异常类类型实例的引用,在本程序中,使用new创建Exception或者RuntimeException 的实例之后将它们(它们的引用)抛出;另外,不可以指定Throwable的下位类之外的类(的实例的引用)(如果指定的话, 就会发生编译错误)
test方法
test方法只用于调用check方法。无论是程序员还是编译器,都知道本方法调用的check中可能会发生检查异常Exception。因此, test方法中也可能会发生检查异常Exception, 必须指定throws子句
检查异常的捕获
main方法中读入变量SW的值后会调用test方法,可能发生检查异常的代码(此处为test(sw)的凋用)放在try语句的try语句块中
捕获的异常的层次
catch子句的形参e声明为Exception 类型如运行结果所示, 这个异常处理器中可以捕获Exception和RuntimeException两种异常;
这是因为存在以下规则:异常处理器会接收形参类型中“可以赋入的所有异常“ 因此.除了catch子句的形参中指定的类类型的异常之外,其下位类类型的异常也可以被捕获。
四、检查异常的处理
下面是一个处理检查异常的程序示例,运行时会显示上次运行时输入的 “心情“; 不过第一次运行时则会显示这是第一次运行的信息
package com.example;// 显示上次的心情
import java.io.*;
import java.util.Scanner;
class LastTime1 {
//--- 读入上次的心情---//
static void init() {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("LastTime.txt"));
String kibun = br.readLine();
System.out.println("上次的心情" + kibun + "。");
} catch (IOException e){
System.out.println("这是您第一次运行本程序。");
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e){
System.out.println("文件关闭失败。");
}
}
}
}
//--- 读入此次的心情---//
static void term(String kibun) {
FileWriter fw = null;
try {
fw = new FileWriter("LastTime.txt");
fw.write(kibun);
} catch (IOException e){
System.out.println("发生错误!!");
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e){
System.out.println("文件关闭失败。");
}
}
}
}
public static void main(String[] args) {
Scanner stdIn = new Scanner(System.in);
init(); // 显示上次的心情
System.out.print("当前的心情:");
String kibun = stdIn.next();
term(kibun);
}
}
输出:
init方法
这是程序最先执行的方法,打开" LastTime.txt" 文件,将第1行的字符串读入到kibun中, 显示上次的心情。
不过,在第l次运行时(或者因某种原因导致文件变得异常等时) ,文件打开或读入会发生异常,在捕获异常的catch子句中,会显示“这是您第1 次运行本程序。”
term方法
这是程序最后执行的方法打开"LastTime.txt"文件,写入字符串kibun,这两个方法中的下述两个地方都可能会发生IOException异常:
1)打开文件时(其与BufferedReader相关联时)
2)对文件实际执行输入/输出时
当1成功、2发生异常时,必须执行文件的关闭处理。因此,文件的关闭处理要放在不管是否发生异常都一定会被执行的finally子句中。在finally子句中,当br或fw不为null时(当文件打开成功时),就会调用close方法,执行关闭处理。;
不过由于关闭处理本身也可能会发生异常,因此close方法的调用代码必须放在try语句的try语句块中,由此造成程序的结构变得非常复杂。
五、创建异常类
Java的类库中提供了为数众多的异常类,既可以直接使用这些异常类、也可以创建自己的异常类。
下面对Exception类或者其下位类进行派生来创建异常类。不过如果要创建的是非检查异常类则要对RuntirneException或者其下位类进行派生
package com.example;// 进行1位(0~9)的加法运算
import java.util.Scanner;
//---- 超出范围的异常 ---//
class RangeError extends RuntimeException {
RangeError(int n) { super("超出范围的数值:" + n); }
}
//---- 超出范围的异常(形参)---//
class ParameterRangeError extends RangeError {
ParameterRangeError(int n) { super(n); }
}
//---- 超出范围的异常(返回值)---//
class ResultRangeError extends RangeError {
ResultRangeError(int n) { super(n); }
}
public class RangeErrorTester {
/*--- n为1位(0~9)吗? ---*/
static boolean isValid(int n) {
return n >= 0 && n <= 9;
}
/*--- 计算1位(0~9)整数a与b的和 ---*/
static int add(int a, int b) throws ParameterRangeError, ResultRangeError {
if (!isValid(a)) throw new ParameterRangeError(a);
if (!isValid(b)) throw new ParameterRangeError(b);
int result = a + b;
if (!isValid(result)) throw new ResultRangeError(result);
return result;
}
public static void main(String[] args) {
Scanner stdIn = new Scanner(System.in);
System.out.print("整数a:"); int a = stdIn.nextInt();
System.out.print("整数b:"); int b = stdIn.nextInt();
try {
System.out.println("它们的和为" + add(a, b) + "。");
} catch (ParameterRangeError e) {
System.out.println("加数超出范围。" + e.getMessage());
} catch (ResultRangeError e) {
System.out.println("计算结果超出范围。" + e.toString());
}
}
}
输出:
程序中创建了三个异常类:后面的两个类都派生自RangeError,这三个类中都只定义了构造函数:
类RangeError派生自RuntimeException 类,是非检查异常类。在构造函数中, 通过将字符串传给super, 调用构造函数来设置详细消息。
剩下的 ParameterRangeError 和 ResultRangeError 由于是RuntimeException 的下位类,因此也是非检查异常类, 在构造函数中,调用super来设置详细消息。
在方法add 中,当参数和加法运符的结果超过 1 位时,就会抛出 ParameterRangeError 或者 ResultRangeError 异常, main方法中会捕获这些异常。
六、委托异常
如果在所有层次的方法中都对数组的下标是否正确(或者本程序中省略的、 接收到的数组变量是否为null) 执行异常处理 , 那么本质上相同的检查就会被执行很多次, 降低软件的性能。
应该在哪个(层次的)方法中执行异常处理,要视软件而异。这里选择在 reverse方法中执行处理,在 swap方法中则不执行处理,如下代码所示
package com.example;// 将值读入到数组元素中,并进行倒序排列(存在Bug:在reverse中捕获异常)
import java.util.Scanner;
class ReverseArray3 {
//--- 交换数组中的元素a[idx1]和a[idx2] ---//
static void swap(int[] a, int idx1, int idx2) {
int t = a[idx1];
a[idx1] = a[idx2];
a[idx2] = t;
}
//--- 对数组a的元素进行倒序排列(错误)---//
static void reverse(int[] a) {
try {
for (int i = 0; i < a.length / 2; i++)
swap(a, i, a.length - i);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.exit(1);
}
}
public static void main(String[] args) {
Scanner stdIn = new Scanner(System.in);
System.out.print("元素个数:");
int num = stdIn.nextInt(); // 元素个数
int[] x = new int[num]; // 元素个数为num的数组
for (int i = 0; i < num; i++) {
System.out.print("x[" + i + "] : ");
x[i] = stdIn.nextInt();
}
reverse(x); // 对数组x的元素进行倒序排列
System.out.println("元素的倒序排列执行完毕。");
for (int i = 0; i < num; i++)
System.out.println("x[" + i + "] = " + x[i]);
}
}
输出:
由于swap方法中并未对异常进行处理,因此当抛出ArrayindexOutOfBoundsException 异常时,异常会传递给调用它的reverse方法;也就是说,swap方法中没有对异常进行处理,而是进行了委托; 这里委托的是非检查异常,如果委托的是检查异常, 那么在方法的声明中就需要包含throws子句。对异常执行处理的是reverse方法,将swap的调用放到try语句块中来捕获异常、
异常处理器中执行了下述操作:
1)调用printStackTrace方法显示栈跟踪
2)调用System.exit方法强制结束程序
七、再次抛出异常
如果 reverse 方法中接收到异常,则将其作为其他异常进行抛出,程序如下:
package com.example;// 将值读入到数组元素中,并进行倒序排列(存在Bug:reverse再次抛出异常)
import java.util.Scanner;
class ReverseArray4 {
//--- 交换数组中的元素a[idx1]和a[idx2] ---//
static void swap(int[] a, int idx1, int idx2) {
int t = a[idx1];
a[idx1] = a[idx2];
a[idx2] = t;
}
//--- 对数组a的元素进行倒序排列(错误)---//
static void reverse(int[] a) {
try {
for (int i = 0; i < a.length / 2; i++)
swap(a, i, a.length - i);
} catch (ArrayIndexOutOfBoundsException e) {
throw new RuntimeException("reverse的Bug?", e);
}
}
public static void main(String[] args) {
Scanner stdIn = new Scanner(System.in);
System.out.print("元素个数:");
int num = stdIn.nextInt(); // 元素个数
int[] x = new int[num]; // 元素个数为num的数组
for (int i = 0; i < num; i++) {
System.out.print("x[" + i + "] : ");
x[i] = stdIn.nextInt();
}
try {
reverse(x); // 对数组x的元素进行倒序排列
System.out.println("元素的倒序排列执行完毕。");
for (int i = 0; i < num; i++)
System.out.println("x[" + i + "] = " + x[i]);
} catch (RuntimeException e) {
System.out.println("异常 :" + e);
System.out.println("异常原因 :" + e.getCause());
}
}
}
输出:
在reverse 方法中,当接收到 ArrayindexOutOfBoundsException 异常时, 处理方法是新创建一个RuntimeException 异常,并将其抛出。
throw new RuntimeException("reverse的Bug?", e);
上面这行代码将2个参数传递给了构造函数。第1个参数为"详细消息",第2个参数为"原因";通过传入第2个参数e, 即ArrayindexOutOfBoundsException异常的引用, 就可以知逍RuntimeException异常发生的原因是ArrayindexOutOfBoundsException异常
main方法中会捕获reverse方法抛出的异常:
catch (RuntimeException e) {
System.out.println("异常 :" + e);
System.out.println("异常原因 :" + e.getCause());
getCause方法用于检查异常的原因, 因此这里会显示捕获的异常及异常原因显示的运行结果是,捕获的异常为RuntimeException, 异常原因为(使用了不正确的下标5导致的) ArrayindexOutOfBoundsException异常。
八、总结
1)所谓异常,就是与程序预期的状态不一致的状态,或者在通常情况下未预料到(或无法预料)的状态;
2)在大多数情况下,异常或者错误的处理方法并不是由控件的开发人员决定的, 而是应该由使用人员来决定;
3)通过异常处理,即对异常执行的处理,程序能够从可能致命的状态中恢复过来;
4)throw语句用于抛出异常;
5)try语句用于捕获抛出的异常并对异常进行处理;
6)对抛出的异常进行检查所需的代码要放在try语句块中,对try语句块中检测出的异常进行捕获的是被称为异常处理器的catch子句;
7)无论是否发生异常,位于try语句末尾的finally子句都会被执行。另外finally子句可以省略;
8)异常主体是Throwable类的下位类的实例,包含详细消息和异常发生的原因等信息
9)检查异常是必须处理的异常,编译时会检查程序中是否对其进行了处理(捕获或者列举在throws子句中);
10)当方法可能会抛出检查异常时,必须将这些异常列举在throws子句中;
11)非检查异常是并非一定要处理的异常,编译时不会检查是否对其进行了处理;
12)Throwable类的子类有Exception类和RuntirneException类;
13)Exception类及其下位类为检查异常,但RuntirneException及其下位类为非检查异常;
14)对于捕获的异常,在执行了某些处理后仍无法完全处理时,可以(直接或者改变形式)再次抛出异常。
package com.example;
import java.util.Scanner;
//---- 自己创建的检查异常 ---//
class CheckedException extends Exception {
CheckedException(String s, Throwable e) { super(s, e); }
}
//---- 自己创建的非检查异常 ---//
class UncheckedException extends RuntimeException {
UncheckedException(String s, Throwable e) { super(s, e); }
}
public class Abc {
//--- 发生sw值所对应的异常 ---//
static void work(int sw) throws Exception {
switch (sw) {
case 1: throw new RuntimeException("发生非检查异常!!");
case 2: throw new Exception("发生检查异常!!");
}
}
//--- 调用work ---//
static void test(int sw) throws CheckedException {
try {
work(sw);
} catch (RuntimeException e) {
/* 虽然试着处理了,但仍无法完全处理 */
throw new UncheckedException("无法处理非检查异常!!", e);
} catch (Exception e) {
/* 虽然试着处理了,但仍无法完全处理 */
throw new CheckedException("无法处理检查异常!!", e);
}
}
public static void main(String[] args) {
Scanner stdIn = new Scanner(System.in);
System.out.print("sw:");
int sw = stdIn.nextInt();
try {
test(sw);
} catch (Exception e) {
System.out.println("异常 :" + e);
System.out.println("异常原因 :" + e.getCause());
e.printStackTrace();
}
}
}
输出: