23、Java基础---异常处理

异常处理

一、异常类

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();
		}
	}
}

输出:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值