1、异常简介
- 所谓异常,从字面上来看就是意外、例外的意思。即在程序运行过程中,意外发生的情况,背离我们程序本身的意图的表现,都可以理解为异常。
- Java提供了强大的异常处理机制,通过合理的异常处理,可以提高程序的健壮性。
- 在Java当中是通过Throwable及其相关子类来对各种异常进行描述的。Throwable是Java异常的根类,它有两个重要的子类Error和Exception。
- Error:是程序无法处理的错误,表示运行应用程序中较严重的问题。常见的错误有虚拟机错误、内存溢出、线程死锁等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于发生的错误,我们也无法通过异常处理来去解决它所引起的各种状况。所以针对Error及其子类所产生的异常,我们通常是不需要关心的。
- Exception:是程序本身可以处理的异常。通常我们提到的异常处理就是针对Exception及其子类的处理。Exception包括非检查异常(UncheckedException)和检查异常(CheckedException)。
- 非检查异常:编译器不要求强制处理的异常,它包括RuntimeException及其相关子类。像空指针异常、数组下标越界、算数异常、类型转换异常等等。即在编写代码时,可以选择捕获这些异常,也可以放任不管,而编译器是不会针对这些异常给出任何错误提示信息的。
- 检查异常:编译器要求必须处理的异常。
- 下面我们主要是针对Exception及其子类进行相关的异常处理。
2、异常处理简介
- 在Java程序中,异常的处理机制通常分为:抛出异常和捕获异常。当然啦,异常只有先被抛出然后才能被捕获(嘿嘿~~)。
- 抛出异常:比如当一个方法中出现错误引发异常时,该方法会去创建异常对象,并且交付给运行时系统进行处理。在这个异常对象当中通常会包含:异常类型、异常出现时的程序状态等信息。
- 捕获异常:当运行时系统捕获到异常,此时,运行时系统就会去寻找合适的处理器,如果找到了与抛出的异常匹配的处理器,那么就会执行相关的处理逻辑;如果始终没有找到,那么运行时系统就会中止,也就意味着Java程序停止运行。
- 对于检查异常(CheckedException)必须捕获或者声明抛出;
- 对于非检查异常(Runtime及其子类)和Error则允许忽略。
- 针对抛出异常和捕获异常,在Java中是通过5个关键字来实现的,即
try、catch、finally、throw
及throws
。
- 捕获异常:
- try:执行可能产生异常的代码。
- catch:捕获异常。
- finally:无论是否发生异常,代码总是执行。
- 抛出异常:
- throw:手动抛出异常。
- throws:声明可能要抛出的异常。
- 捕获异常:
3、使用try…catch…finally实现异常处理
public void method(){
try {
// 代码段1
// 产生异常的代码段2
} catch (Exception e) {
// 对异常进行处理的代码段3
} finally {
// 代码段4
}
}
- try块:用于捕获异常。
- catch块:用于针对try块当中捕获到的异常进行处理。
- finally块:无论是否发生异常代码总会执行。
- try块后可以零个或多个catch块,如果没有catch块,则必须跟一个finally块。简单来说,就是try必须和catch或者finally组合使用。
3.1、try…catch块
- 1.下面进行代码演示,首先创建一个名为
ExceptionProj
的Java项目,并新建一个名为com.cxs.test
测试包,在包下新建一个名为TryCatchDemo
的类,并选中main方法。
public class TryCatchDemo {
public static void main(String[] args) {
Scanner keyboardInput = new Scanner(System.in);
System.out.print("请输入第一个整数:");
int one = keyboardInput.nextInt();
System.out.print("请输入第一个整数:");
int two = keyboardInput.nextInt();
System.out.println("one/two=" + one / two);
keyboardInput.close();
}
}
- 2.运行代码,按照下图进行尝试。
- 3.下面我们来使用
try...catch
块去捕获异常。按下图进行快速操作,并进行代码修改。
public class TryCatchDemo {
public static void main(String[] args) {
System.out.println("运算开始!");
Scanner keyboardInput = new Scanner(System.in);
try {
System.out.print("请输入第一个整数:");
int one = keyboardInput.nextInt();
System.out.print("请输入第一个整数:");
int two = keyboardInput.nextInt();
System.out.println("one/two=" + one / two);
} catch (ArithmeticException e) {
System.out.println("您输入的除数为0!");
e.printStackTrace();
} catch (InputMismatchException e) {
System.out.println("您输入的为非数字字符!请输入整数");
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
keyboardInput.close();
System.out.println("运算结束!");
}
}
注意:
- try后面跟着多个catch块时,不能出现同类型异常;
- 父类Exception异常应该放到最后来进行捕获,这样也可以捕获到前面的catch块无法捕获的异常信息,而最终捕获到异常,都会被父类Exception收入囊中(嘿嘿~~),这也算是一种安全保障机制。
- catch和finally是不能脱离try而单独存在的(这里没有添加finally,大家可以自行尝试)。
- 4.运行代码,分别进行尝试。与不捕获异常相比,捕获异常后,我们可以继续向下执行代码(运算结束被打印出来,代表程序完整地运行了下来)。
3.2、中止finally的执行
通过前面所讲我们知道,通常情况下,在finally中的代码段,无论是否发生异常代码总会执行。那么什么情况下,会中止finally的执行呢?
- 5.修改一下代码,如下所示,将打印运算结束的语句放在finally中,并在捕获算数异常的catch块当中添加一句代码,如下所示。
public class TryCatchDemo {
public static void main(String[] args) {
System.out.println("运算开始!");
Scanner keyboardInput = new Scanner(System.in);
try {
System.out.print("请输入第一个整数:");
int one = keyboardInput.nextInt();
System.out.print("请输入第一个整数:");
int two = keyboardInput.nextInt();
System.out.println("one/two=" + one / two);
} catch (ArithmeticException e) {
System.exit(1);//←添加改行代码
System.out.println("您输入的除数为0!");
e.printStackTrace();
} catch (InputMismatchException e) {
System.out.println("您输入的为非数字字符!请输入整数");
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("运算结束!");
}
keyboardInput.close();
}
}
- 6.运行代码,这里我们就做一个捕获算数异常的尝试,结果如下所示,我们再输入0后,finally快中的输出语句并没有执行,并且控制台的停止按钮并不是红色,而显示灰色,对,此时程序已经停止了运行。这便是
System.exit(1);
所起的作用。
- 7.我们去官方API查看一下System的该方法介绍,如下图,作用为:终止当前正在运行的Java虚拟机。
- 8.点进去看看exit中的参数含义,如下图:参数用作状态代码,按照惯例,非零状态代码表示异常终止。
3.3、return关键字在异常处理中的作用
- 通过前面的学习我们知道,return可以中止方法的执行,并将返回值带回调用处。
- 9.新建一个测试类Test,下面继续使用上面的代码,并做一定的修改。
public class Test {
public static int test() {
System.out.println("运算开始!");
Scanner keyboardInput = new Scanner(System.in);
try {
System.out.print("请输入第一个整数:");
int one = keyboardInput.nextInt();
System.out.print("请输入第一个整数:");
int two = keyboardInput.nextInt();
return one / two;
} catch (ArithmeticException e) {
System.out.println("您输入的除数为0!");
return 0;
} finally {
System.out.println("运算结束!");
keyboardInput.close();
return 10000;
}
}
public static void main(String[] args) {
int result = test();
System.out.println("one和two的商为:" + result);
}
}
- 10.运行代码,并做相应的尝试,结果如下。发现,无论是正常执行,还是捕获算数异常,最终返回的都为finally中的值(由finally的强制执行造成的结果,这里可以尝试打断点调试,查看程序执行的顺序)。
- 11.其实我们在finally中编写return语句时,编译器就给出相应的黄色波浪线警告了,如下图所示。
- 12.此时将finally块中return语句注释,则警告消失,再次运行代码,便会得到符合预期的结果(结果略)。
4、使用throw和throws实现异常处理
可以通过throws声明将要抛出何种类型的异常,通过throw将产生的异常抛出。 |
4.1、使用throws声明异常类型
- 如果一个方法可能会出现异常,但没有能力处理这种异常。可以在方法声明处用throws子句来声明抛出异常。如下图所示,throws后面跟的是异常类型,并不是异常对象。
public void method() throws Exception1,Exception2,...,ExceptionN{
//可能产生异常的代码
}
- 当方法抛出异常列表中的异常时,方法将不对这些类型及其子类类型的异常做处理,而抛向调用该方法的方法,由调用方法去处理异常。
- 1.在test包下新建一个名为
ThrowsDemo
类,并勾选main
方法,将上面的代码复制一份,修改代码如下。
public class ThrowsDemo {
public static int test() throws ArithmeticException, InputMismatchException {
System.out.println("运算开始!");
Scanner keyboardInput = new Scanner(System.in);
System.out.print("请输入第一个整数:");
int one = keyboardInput.nextInt();
System.out.print("请输入第一个整数:");
int two = keyboardInput.nextInt();
System.out.println("运算结束!");
keyboardInput.close();
return one / two;
}
public static void main(String[] args) {
test();
}
}
- 2.在上面的代码中,我们再不捕获异常的情况下,编译器并没有报错,原因是算数异常和类型不匹配异常均属于非检查异常,而编译器针对非检查异常可以忽略,因而没有提示错误。
- 3.若在抛出的异常类型中加上Exception,此时在test()调用方法处会报错(因为Exception包含检查异常和非检查异常,而检查异常既然已经声明抛出了,则要求必须对异常进行捕获并处理的)。
- 4.下面我们将代码补充完整,利用快捷键
Alt+Shift+Z
快速完成代码。
public class ThrowsDemo {
public static int test() throws ArithmeticException, InputMismatchException {
System.out.println("运算开始!");
Scanner keyboardInput = new Scanner(System.in);
System.out.print("请输入第一个整数:");
int one = keyboardInput.nextInt();
System.out.print("请输入第一个整数:");
int two = keyboardInput.nextInt();
System.out.println("运算结束!");
keyboardInput.close();
return one / two;
}
public static void main(String[] args) {
try {
int result = test();
System.out.println("one和two的商为:" + result);
} catch (InputMismatchException e) {
System.out.println("您输入的为非数字字符,请输入整数!");
e.printStackTrace();
} catch (ArithmeticException e) {
System.out.println("您输入的除数为0!");
e.printStackTrace();
} catch (Exception e) {// 为保险起见,此处加上捕获Exception
System.out.println("程序出错了!");
e.printStackTrace();
}
}
}
4.2、使用throw抛出异常对象
- throw用来抛出异常。例如:
throw new IOException();
- throw抛出的只能是可抛出类Throwable或其子类的实例对象。
- 当方法中包含throw抛出对象的语句时,通常有两种处理方式:
- ①在throw语句外套上
try...catch
块,即自己抛出自己处理; - ②在抛出异常的方法声明处通过throws关键字标识对应的异常类型,即谁调用谁处理。
- ①在throw语句外套上
public method (){//方式1
try{
//代码段1
throw new 异常类型();
} catch(异常类型 e){
//对异常进行处理的代码段2
}
}
public void method() throws 异常类型{//方式2
//代码段1
throw new 异常类型();
}
- 1.下面进行代码实践。在
test
包下新建一个名为ThrowDemo
的类,并勾选上主方法。 2.定义一个
testAge
方法,如下所示,在利用throw关键字抛出异常时,编译器会给出以下提示,正好对应上面所讲的两种方式。
3.根据上面的代码编辑器给出的提示,我们先采用方式1,如下图所示,声明Throwable或Exception均可。
这里将throw new Exception()提取为warning静态方法,否则编译器会给出黄色波浪线警告
public class ThrowDemo {
public static void main(String[] args) {
try {// 在调用处进行异常处理,当然此时还可以继续向上抛出,交给虚拟机处理
testAge();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void testAge() throws Exception {
System.out.println("请输入您的年龄:");
Scanner input = new Scanner(System.in);
int age = input.nextInt();
if (age < 18 || age > 80) {
warning();
} else {
System.out.println("欢迎入住本酒店!");
}
input.close();
}
private static void warning() throws Exception {
throw new Exception("18岁以下,80岁以上的住客必须由亲友的陪同才能入住!");
}
}
- 4.运行程序,输入82,结果如下。
- 5.再来看一下方式2。
public class ThrowDemo {
public static void main(String[] args) {
testAge();
}
public static void testAge() {
try {
System.out.println("请输入您的年龄:");
Scanner input = new Scanner(System.in);
int age = input.nextInt();
if (age < 18 || age > 80) {
warning();
} else {
System.out.println("欢迎入住本酒店!");
}
input.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void warning() throws Exception {
throw new Exception("18岁以下,80岁以上的住客必须由亲友的陪同才能入住!");
}
}
- 6.运行代码,输入年龄2,结果如下。
5、自定义异常
- 使用Java内置的异常类可以描述在编程时出现的大部分异常情况。
- 当然,也可以通过自定义异常描述特定业务产生的异常类型。
- 所谓的自定义异常,就是定义一个类,去继承Throwable类或它的子类。
- 1.在test包下新建一个名为
HotelAgeException
,并继承自Exception类。
public class HotelAgeException extends Exception {
public HotelAgeException() {
super("18岁以下,80岁以上的住客必须由亲友的陪同才能入住!");
}
}
- 2.修改ThrowDemo的代码。
public class ThrowDemo {
public static void main(String[] args) {
try {// 在调用处进行异常处理,当然此时还可以继续向上抛出,交给虚拟机处理
testAge();
} catch (HotelAgeException e) {
System.out.println(e.getMessage());
System.out.println("酒店前台工作人员不允许办理入住登记!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("除自定义异常之外,其余的异常在这里进行处理!");
}
}
public static void testAge() throws HotelAgeException {
System.out.println("请输入您的年龄:");
Scanner input = new Scanner(System.in);
int age = input.nextInt();
if (age < 18 || age > 80) {
throw new HotelAgeException();
} else {
System.out.println("欢迎入住本酒店!");
}
input.close();
}
}
- 3.运行代码,做三次尝试,结果如下所示。
6、异常链
异常链是一种面向对象编程技术,指将捕获的异常包装进一个新的异常中并重新抛出的异常处理方式。原异常被保存为新异常的一个属性(比如cause)。【引自百科】
- 1.创建一个名为
ExceptionChain
类,并勾选主方法。在其中定义三个静态方法,用于异常的连续抛出,在主方法中对抛出的异常进行处理。
public class ExceptionChain {
public static void main(String[] args) {
try {
testThree();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void testOne() throws HotelAgeException {
throw new HotelAgeException();
}
public static void testTwo() throws Exception {
try {
testOne();
} catch (HotelAgeException e) {
throw new Exception("我是新产生的异常1");
}
}
public static void testThree() throws Exception {
try {
testTwo();
} catch (Exception e) {
throw new Exception("我是新产生的异常2");
}
}
}
- 2.运行代码,发现只获取到了最后一个方法的异常信息,即丢失了前两个方法的异常信息。
针对异常信息的丢失该如何解决呢?有办法将前面的异常信息保留下来吗?不要担心,在Java的API中提供了保留异常的机制,我们来看一下Throwable的帮助文档。 |
- 构造器
Throwable(String message, Throwable cause)
:用指定的详细信息和原因构造一个新的可投掷的 - 方法
initCause(Throwable cause)
:用一个异常信息初始化一个新的异常。
- 3.采用上面的构造器的方法,在
testTwo
和testThree
方法中分别采用构造器Throwable(String message, Throwable cause)
和方法initCause(Throwable cause)
将原异常信息添加到新的异常当中并抛出。
public static void testTwo() throws Exception {
try {
testOne();
} catch (HotelAgeException e) {
throw new Exception("我是新产生的异常1", e);// ←——将捕获的异常作为构造器的第二个参数加进来
}
}
public static void testThree() throws Exception {
try {
testTwo();
} catch (Exception e) {
// ←——采用initCause方法将原异常信息添加到新的异常对象中
Exception e2 = new Exception("我是新产生的异常2");
e2.initCause(e);
throw e2;
}
}
- 4.运行代码,结果如下所示。
总结:随着异常从底层逐层抛出传给上层,然后将这些异常发生的原因串连起来,这便是异常链。