JAVA编程讲义之异常处理
异常是指正常执行的程序遇到了非正常的情况,导致程序执行中断。虽然Java语言的设计从根本上提供了便于写出整洁、安全的代码的方法,并且程序人员也会尽可能地减少错误的产生,但是会使程序被迫停止的错误仍然是不可避免的。为此,Java提供了异常处理机制来帮助开发人员检查可能出现的错误,从而确保程序的安全性、可维护性和可读性。本章将针对Java程序中异常的产生及处理进行讲解,包含异常的类型、异常的捕获与处理方法以及如何实现自定义异常。
8.1 异常概述
在程序开发过程中,由于开发人员的疏忽或是其他情况都可能导致程序发生异常,而程序一旦发生异常就无法正常有效地执行,不能得到预期的结果。例如:
• 进行算术运算时,除数为零。
• 数组遍历时,下标越界。
• 获取数据时空指针。
上述情况的发生,都会导致程序的中断,影响程序的正常执行。
接下来,通过案例来认识一下异常的发生,如例8-1所示。
例8-1 Demo0801.java
1 package com.aaa.p0801;
2
3 public class Demo0801 {
4 public static void main(String[] args) {
5 int divide = divide(5, 0); // 调用divide()方法
6 System.out.println(divide);
7 }
8
9 // 下面的方法实现了两个整数相除
10 public static int divide(int x, int y) {
11 int result = x / y; // 定义一个变量result,记录两个数相除的结果
12 return result; // 将结果返回
13 }
14 }
程序的运行结果如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.aaa.p0801.Demo0801.divide(Test.java:13)
at com.aaa.p0801.Demo0801.main(Test.java:7)
例8-1中,程序执行出现了ArithmeticException异常,即算术异常,是由除数为0引起的(异常信息提示:在调用divide()方法时除数为0),该异常发生后,系统会停止不再继续执行,这种情况就是所说的异常会导致程序的终止。
为了防止异常的发生,保证程序的正常执行,Java语言专门提供了异常处理机制,开发人员既可以在当前方法中进行异常的捕获和处理,也可以将异常抛出由方法调用者处理。只有这样,才能保证程序不会因为异常的产生而终止。
8.2 异常的类型
Java语言帮我们定义了很多异常类,每一个异常类都代表了一种异常情况,并提供了相应的方法用来返回异常信息,本节我们就来了解异常类的体系结构以及常见的异常类。
8.2.1 异常类的体系结构
在Java异常类的结构体系中,Throwable类是所有异常类的超类,Throwable派生出了两个非常重要的子类:Exception类和Error类。Exception类和Error类又派生出了很多的子类,用来处理不同的异常情况。如图8.1所示,给出了Java异常类的体系结构。
图8.1所示的几个重要异常类的作用如下:
• Error:是Throwanble的一个子类,表示的是错误,错误指的是仅靠程序本身是没有办法恢复的问题,所有程序的内部错误都是Error类及其子类抛出的。例如,VirtualMachineError(虚拟机损坏或资源耗尽)、内存溢出、栈溢出等,通常比较严重,是不可查的,Java也不会对这些错误进行处理。
• Exception:是Throwable的另外一个子类,表示的是可以被程序捕捉并处理的错误情况,我们称之为异常。Exception又派生出了许多子类来处理不同类型的异常。例如,IOException用来处理I/O异常,DataFormatException用来处理数据格式异常,等。
• RuntimeException:是Exception的子类,表示运行时异常,用来捕捉程序在运行期间出现的异常现象,如ArithmeticException(算术异常,除数为0),IndexOutOfBoundsException
(数组下标越界),NullPointerException(空指针异常)等。当RuntimeException出现时,就表示是开发人员在设计时出现了错误。
图8.1 异常类的结构体系
8.2.2 常见的异常类型
在程序开发过程中,有些异常发生在程序编译时期,有些异常发生在程序运行时期。接下来,我们对这两种情况做详细介绍。
1.编译时异常
编译时异常要求开发人员必须在程序编译期间就进行处理,否则程序无法正常编译,这种异常也称checked异常。Java程序在编译的过程中,编译器会对代码进行检查,如果发现明显的异常,程序无法通过编译,就要求开发人员进行处理。在Exception的子类中,除了RuntimeException类及其子类外,其他类都是编译时异常。如表8.1所示,列出了常见的编译时异常。
表8.1 常见编译时异常
异常类名称 |
说 明 |
IOException |
I/O异常 |
SQLException |
数据库访问异常 |
DateFormatException |
数据格式异常 |
ParserException |
解析异常 |
接下来,我们通过案例来认识一下什么是编译时异常,如例8-2所示
例8-2 Demo0802.java
1 package com.aaa.p080202;
2
3 public class Demo0802 {
4 public static void main(String[] args) {
5 // new FileWriter("demo.txt")报错
6 FileWriter fw = new FileWriter("demo.txt");
7 }
8 }
例8-2中,第6行代码存在编译时异常,编译器检查会报IOException异常。但是,在语法上代码是正确的,那么为什么编译器会报错呢?这是因为我们调用的new FileWriter("demo.txt")这个构造方法中使用throws进行了异常抛出,所以在使用该类构造方法进行对象时,要求必须进行异常处理。从这我们看出,所有的编译异常都是Java的源码本身抛出异常,我们在使用的时候就必须处理异常,否则编译无法通过,最终导致我们的程序无法运行。如图8.2所示,给出了程序发生编译时异常的本质原因。
图8.2 编译时异常的本质原因
2.运行时异常
运行时异常不会像编译时异常那样,强制要求开发人员必须在程序编译期间对这些异常进行处理,程序可以正常通过编译然后运行,但是,在程序运行期间可能会因为出错导致程序的终止,这类异常也称unchecked异常。RuntimeException类及其子类都是运行时异常。如表8.2所示,列出了常见的运行时异常。
表8.2 常见运行时异常
异常类名称 |
说 明 |
ArithmeticException |
算术异常 |
IndexOutOfBoundsException |
索引越界异常 |
ClassCastException |
类型转换异常 |
NullPointerException |
空指针异常 |
运行时异常一般是由程序中的逻辑错误引起的,在程序运行时无法恢复。例如,通过数组的下标访问数组的元素时,如果超过了数组的最大索引,就会发生运行时异常,示例代码如下:
int [] arr = new int[5];
Sysstem.out.println(arr[5]);
上面的代码中,由于数组arr的长度为5,最大索引值应为4,当使用arr[5]访问数组中的元素时就会发生数组索引越界的异常。
8.3 try-catch捕获异常
例8-1的的程序发生了算术异常,那么Java语言是如何解决这类异常的呢?Java语言提供了异常捕获的处理机制,可以帮助我们处理由异常引发的问题。一般情况下,异常捕获使用try-catch语句,其语法格式如下:
try {
可能会产生异常的程序代码
} catch (异常类型 e) {
发生异常后处理的程序代码1
}catch (异常类型 e) {
发生异常后处理的程序代码2
} … catch (异常类型 e) {
发生异常后处理的程序代码n
}
上述语法中,try的花括号里面放的是可能会产生异常的代码;一个异常代码块可以被一个或是多个catch语句进行捕获,当有多个catch语句时,try里面代码发生异常的时,则会依次判断catch语句小括号里的异常类型是否和try里发生的异常匹配,匹配则成功捕捉,不匹配则继续向下进行匹配;如果只有一个catch语句,并且不匹配,则异常捕捉失败,程序依然会中断执行;每个catch语句块可以处理的异常类型由异常处理器参数指定,那么什么是异常处理参数呢,就是我们在图8.1中所示的各种异常类型。
接下来,我们使用try-catch语句对例8-1中出现的异常进行捕获,如例8-3所示。
例8-3 Demo0803.java
1 package com.aaa.p0803;
2
3 public class Demo080301 {
4 public static void main(String[] args) {
5 try {
6 final int divide = divide(5, 0); // 调用divide方法
7 System.out.println(divide);
8 } catch (ArithmeticException e) { // 注意异常类型,不匹配则捕获失败
9 System.out.println("捕获到了异常:" + e); // 异常处理语句
10 }
11 System.out.println("异常捕获结束");
12 }
13
14 // 下面的方法实现了两个整数相除
15 public static int divide(int x, int y) {
16 int result = x / y; // 定义一个变量result,记录两个数相除的结果
17 return result; // 将结果返回
18 }
19 }
程序运行结果如下:
捕获到了异常:java.lang.ArithmeticException: / by zero
异常捕获结束
例8-3中,我们对可能发生异常的代码使用了try-catch语句进行捕获处理,在try代码块中发生了java.lang.ArithmeticException: / by zero异常,即算术异常,程序跳转到catch语句中的执行。从运行结果可发现,在try语句块中,当程序发生异常时,try语句块中发生异常部分的后面的代码不再被执行。在catch代码块中,系统对异常进行处理,处理完成后程序正常向后执行,程序不再因为发生异常而终止执行。
8.4 finally进行清理
事实上,一个完整的异常处理语句由3部分组成,即try语句块、catch代码块和finally语句块,结构如下:
try {
可能会产生异常的程序代码
}catch (异常类型 e) {
发生异常后处理的程序代码1
}catch (异常类型 e) {
发生异常后处理的程序代码2
} … catch (异常类型 e) {
发生异常后处理的程序代码n
}
finally {
最终执行的程序代码
}
从语法上来讲,当异常处理机制结构中出现catch语句块时finally语句块是可选的,当包含finally语句块时catch语句块是可选的,二者可以同时存在,也可以只存在其一,不像catch可以出现多次,finally语句块只能出现一次。
8.4.1 finally用来做什么
finally是最终执行的代码块,无论try...catch块中是否发生异常,finally语句块中的代码都会被执行。也就是说,当希望程序中的某些语句无论程序是否发生异常都执行时,可以将这些语句打包成finally语句块。在实际开发中,finally语句块用于关闭文件或释放其他系统资源。当然,也有一些例外情况,就是在try-catch语句块中执行System.exit(0)语句,表示退出当前的Java虚拟机,Java虚拟机停止了,程序中的任何代码都不会再执行了。
接下来,通过案例来演示finally语句块的使用,如例8-4所示。
例8-4 Demo0804.java
1 package com.aaa.080401;
2
3 public class Demo0804 {
4 public static void main(String[] args) {
5 try {
6 final int result = divide(5, 0); // 调用divide方法
7 System.out.println(result); // 打印结果
8
9 } catch (ArithmeticException e) {
10 System.out.println("捕获到了异常:" + e);
11 return;
12 } finally {
13 System.out.println("开始执行finally块");
14 }
15 System.out.println("异常捕获结束");
16 }
17 // 下面的方法实现了两个整数相除
18 public static int divide(int x, int y) {
19 int result = x / y; // 定义一个变量result记录两个数的商
20 return result; // 将结果返回
21 }
22 }
程序运行结果如下:
捕获到了异常:java.lang.ArithmeticException: / by zero
开始执行finally块
例8-4中,在catch语句块中添加了return语句,return语句的作用在于结束当前方法。从程序运行的结果中不难发现,finally语句块中的程序仍会被执行,不会受return语句的影响,而try-catch-finally结构后面的代码就不会被执行。由此我们不难看出,不管程序是否发生异常,也不论在try和catch语句块中是否使用return语句结束,finally语句块都会被执行。
另外,finally是在return后面的表达式运算完成之后才执行的,此时并没有直接返回运算值,而是先将返回值保存(假设保存在了内存A中),finally中的代码的执行不会影响方法的返回值,仍是在finally执行之前内存A中的值,因此,方法返回值是在finally执行之前就已经确定的。
接下来,通过案例对上述情况进一步说明,如例8-5所示。
例8-5 Demo0805.java
1 Package com.aaa.p080401;
2
3 public class Demo0805 {
4 public static int testReturn() {
5 int x = 1; // 定义变量x
6 try {
7 ++x; // ++在前时先运算
8 return x; // 返回结果
9 } finally {
10 ++x; // ++在前时先运算
11 System.out.println("finally:" + x); // 打印在finally中的结果
12 }
13 }
14 public static void main(String[] args) {
15 System.out.println("最终返回值:" + testReturn());
16 }
17 }
程序运行结果如下:
finally:3
最终返回值:2
例8-5中,随着方法被调用,程序先执行try代码块中++x,此时x的值为2;然后执行return语句时,先将计算结果2保存;接着程序转而执行finally语句块中++x,此时x的值为3;执行完毕之后,再从中取出返回结果进行返回。因此,虽然finally中对变量x进行了++操作,但是没有影响最终的返回结果。
8.4.2 在finally中使用return
在finally中最好不要使用return语句,否则程序会提前退出,返回值不是try或catch语句块中保存的返回值。
接下来,我们在try和finally语句块中加return语句进行测试,如例8-6所示。
例8-6 Demo0806.java
1 Package com.aaa.p080402;
2
3 public class Demo0806 {
4 public static String testReturn() {
5 try {
6 return "我是try中的return";
7 } finally {
8 return "我是finally中的return";
9 }
10 }
11 public static void main(String[] args) {
12 System.out.println("返回结果:" + testReturn());
13 }
14 }
程序运行结果如下:
返回结果:我是finally中的return
例8-6中,程序最终输出的是finally中的return语句,而并不是try中的return语句,所以在finally中最好不要使用return语句。
8.5 throws关键字和throw关键字
若某个方法可能会出现异常,但我们又不想在当前方法中处理这个异常,这时我们可以使用throws、throw关键字在方法中抛出异常,由方法调用者进行处理。
8.5.1 使用throws关键字抛出异常
任何代码都有发生异常的可能性,如果方法不想对可能出现的异常做捕获处理,那么方法必须声明它可以抛出的这些异常,用于告知方法调用者此方法存在异常。Java语言通过throws子句声明方法可抛出的异常,throws子句由throws关键字和要抛出的异常类两部分组成,如果需要抛出多个异常类则异常类与异常类之间通过逗号隔开,其语法格式如下:
数据类型 方法名(参数列表) throws 异常类1,异常类2,……,异常类n{
方法体;
}
方法一旦使用throws进行异常抛出,则表示当前方法不再对异常做任何处理,而是由方法调用者来进行处理。此时,无论原方法是否有异常发生,系统都会要求调用者必须对异常进行处理。
接下来,通过案例来演示throws关键字的使用,如例8-8所示。
例8-7 Demo0807.java
1 Package com.aaa.p080501;
2
3 public class Demo0807 {
4 public static void main(String[] args) {
5 try {
6 // 因为方法中声明抛出异常,不管是否发生异常,都必须处理
7 final int divide = divide(5, 0); // 调用divide方法
8 System.out.println(divide); // 打印结果
9 } catch (ArithmeticException e) {
10 System.out.println("异常信息:" + e);
11 }
12 System.out.println("异常捕获结束");
13 }
14
15 // 声明抛出异常,本方法中可以不处理异常
16 public static int divide(int x, int y) throws ArithmeticException {
17 int result = x / y; // 定义一个变量result记录两个数相除的结果
18 return result; // 将结果返回
19 }
20 }
程序运行结果如下:
异常信息:java.lang.ArithmeticException: / by zero
异常捕获结束
例8-7中,在定义divide()方法时,使用了throws关键字声明抛出ArithmeticException异常。main()方法调用该方法时,main()方法中使用了try-catch对异常进行了捕获处理,因此程序才可以正常编译运行。
throws子句不仅仅可以在方法处声明,方法的调用者处也可以声明。如果主方法使用throws声明抛出异常,则异常会被Java虚拟机进行处理(Java虚拟机处理会导致程序终止)。
接下来,通过案例来演示上述情况,如例8-9所示。
例8-8 Demo0808.java
1 package com.aaa.p080501;
2
3 public class Demo0808 {
4 // main方法中不做处理,继续向上抛出异常
5 public static void main(String[] args) throws ArithmeticException {
6 // 主方法在调用时未做捕获处理,因此JVM会直接报异常
7 final int divide = divide(5, 0); // 调用divide方法
8 System.out.println(divide); // 打印结果
9 System.out.println("程序结束");
10 }
11
12 // 下面的方法实现了两个整数相除
13 public static int divide(int x, int y) throws ArithmeticException {
14 int result = x / y; // 定义一个变量result,记录两个数相除的结果
15 return result; // 将结果返回
16 }
17 }
程序运行结果如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.aaa.p080501.Test.divide(Test.java:15)
at com.aaa.p080501.Test.main(Test.java:8)
例8-8中,在使用main()方法调用divide()方法时,并没有对异常进行捕获处理,而是使用throws关键字继续声明抛出异常。从执行结果我们可以发现,程序虽然正常通过编译,但在运行时因为没有对异常进行处理,所以导致程序终止。由此可知,在main()方法中使用throws关键字抛出异常,程序一旦出现异常会由Java虚拟机进行处理,这将导致程序执行中断。
8.5.2 使用throw关键字抛出异常
在前文的案例中,很多异常都是在程序执行期间由Java虚拟机进行捕捉抛出的,但有时我们希望能亲自进行异常类对象的实例化操作,手动进行异常的抛出,这时我们就要使用throw关键字来实现,其语法格式如下:
throw new 异常对象();
接下来,通过案例来演示使用throw关键字手动抛出异常,如例8-9所示。
例8-9 Demo0809.java
1 package com.aaa.p080502;
2
3 public class Demo0809 {
4 public static void main(String[] args) {
5 try {
6 final int divide = divide(5, 0); // 调用divide方法
7 System.out.println(divide); // 打印结果
8 } catch (ArithmeticException e) {
9 e.printStackTrace();
10 }
11 System.out.println("程序结束");
12 }
13
14 // 下面的方法实现了两个整数相除
15 public static int divide(int x, int y) {
16 if (y == 0) {
17 throw new ArithmeticException("错误:除数不能为0!");
18 }
19 int result = x / y; // 定义一个变量result,记录两个数相除的结果
20 return result; // 将结果返回
21 }
22 }
程序运行结果如下:
程序结束
java.lang.ArithmeticException: 错误:除数不能为0!
at com.aaa.p080502.Test.divide(Test.java:19)
at com.aaa.p080502.Test.main(Test.java:8)
例8-9中,在divide()方法中,直接使用throw关键字进行了异常类ArithmeticException的抛出。从程序运行结果中可以发现,异常捕获机制能对throw抛出的异常进行捕获并处理。
通过例8-9发现,Java的异常一般会被JVM拦截并抛出,但是有时可能会遇到因为传参出现的参数问题,或是其他的特殊错误情况需要自定义的异常,这是就需要开发人员进行手动抛出异常。
8.6 异常处理的三种常用形式
通过第8.3节可知,可以使用try-catch-finally块进行异常处理,但是如果一段代码块中处理的异常类型较多时,我们又该以什么样的形式进行处理呢?
下面,我们先来看一段代码,这段代码有两处可能抛出异常,如例8-11所示。
例8-10 Demo0810.java
1 package com.aaa.p080600;
2 import java.util.Scanner;
3
4 public class Demo0810 {
5 public static void main(String[] args) {
6 Scanner scanner = new Scanner(System.in);
7 int result = 0;
8 int number1 = 0;
9 int number2 = 0;
10 // 这里可能会抛出异常
11 System.out.print("number1=");
12 number1 = scanner.nextInt();
13 System.out.print("number2=");
14 number2 = scanner.nextInt();
15 // 这里也可能抛出异常
16 result = number1 / number2;
17 System.out.println(result);
18 }
19 }
例8-10的代码中可能会抛出异常的地方有两个,那么我们应该如何处理呢?
8.6.1 第一种方式:分开捕获
分开捕获就是在可能出现不同异常的地方,分开依次进行捕获处理,这样做的好处就是我们可以非常清晰地知道哪些地方进行了什么异常处理,结构清晰。
接下来,通过案例来演示分开捕获的使用,如例8-12所示。
例8-11 Demo0811.java
1 package com.aaa.p080601;
2 import java.util.Scanner;
3
4 public class Demo0811 {
5 public static void main(String[] args) {
6 Scanner scanner = new Scanner(System.in);
7 int result = 0;
8 int number1 = 0;
9 int number2 = 0;
10 // 这里可能会抛出异常
11 try {
12 System.out.print("number1=");
13 number1 = scanner.nextInt();
14 System.out.print("number2=");
15 number2 = scanner.nextInt();
16 } catch (InputMismatchException e) {
17 e.printStackTrace();
18 }
19 // 这里也可能抛出异常
20 try {
21 result = number1 / number2;
22 System.out.println(result);
23 } catch (ArithmeticException e) {
24 e.printStackTrace();
25 }
26 }
27 }
例8-11的代码中,将可能会抛出异常的两个地方分别使用try-catch语句块进行捕获,这种方式可以解决这两个可能会出现的异常问题。
8.6.2 第二种方式:嵌套捕获
嵌套捕获就是在异常捕获处理语句块中对于可能出现异常的部分再次进行异常捕获处理。异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程可放在try-catch-finally块中的任意位置。异常处理嵌套的深度没有明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且会导致程序可读性降低。
接下来,通过案例来演示嵌套捕获的使用,如例8-13所示。
例8-12 Demo0812.java
1 package com.aaa.p080602;
2 import java.util.Scanner;
3
4 public class Demo0812 {
5 public static void main(String[] args) {
6 Scanner scanner = new Scanner(System.in);
7 int result = 0;
8 int number1 = 0;
9 int number2 = 0;
10 // 这里可能会抛出异常
11 try {
12 System.out.print("number1=");
13 number1 = scanner.nextInt();
14 System.out.print("number2=");
15 number2 = scanner.nextInt();
16
17 // 这里也可能抛出异常
18 try {
19 result = number1 / number2;
20 System.out.println(result);
21 } catch (ArithmeticException e) {
22 e.printStackTrace();
23 }
24 } catch (InputMismatchException e) {
25 e.printStackTrace();
26 }
27 }
28 }
例8-12中,将可能会抛出异常的两个地方通过嵌套的方式使用try-catch进行捕获,这种方式也可以解决这两个可能会出现的异常问题。
8.6.3 第三种方式:联动捕获
我们在例8-11和例8-12中对两个可能会出现异常的位置做了不同的处理,虽然都可以解决这两个问题,但是会发现不管用哪种方式都会写两遍try-catch语句块,甚至第2种方式还会影响代码的可读性。为了解决这种问题,Java提供了另外一种异常捕获处理方式——联动捕获。
所谓联动捕获,具体指的是一个try语句块配合多个catch语句块,当异常发生以后,符合异常情况的catch语句块就会被执行,其他的catch语句块会被跳过。
接下来,通过案例来演示联动捕获的使用,如例8-13所示。
例8-13 Demo0813.java
1 package com.aaa.p080603;
2 import java.util.Scanner;
3
4 public class Demo0813 {
5 public static void main(String[] args) {
6 Scanner scanner = new Scanner(System.in);
7 int result = 0;
8 int number1 = 0;
9 int number2 = 0;
10 // 这里可能会抛出异常
11 try {
12 System.out.print("number1=");
13 number1 = scanner.nextInt();
14 System.out.print("number2=");
15 number2 = scanner.nextInt();
16
17 // 这里也可能抛出异常
18 result = number1 / number2;
19 System.out.println(result);
20 } catch (InputMismatchException e) {
21 e.printStackTrace();
22 } catch (ArithmeticException e) {
23 e.printStackTrace();
24 }
25 }
26 }
例8-13中,将可能会抛出异常的两个地方用try语句块包住,使用两个catch语句块进行捕获不同的异常,这种方式可以有效地解决例8-11和例8-12这两种方式造成的麻烦,并且也能解决可能会出现的异常。
注意:使用联动捕获时,下边的catch语句块捕获的异常类必须是上边catch语句块的同级类或是父级类。
8.7 自定义异常及异常丢失现象
8.7.1 自定义异常
在实际开发中,Java提供的异常类可能不适合用来处理我们的现实业务,这时需要通过自定义异常来完成对实际业务的实现。例如,在统计信息时要求年龄必须合理,这时我们就可以使用自定义异常来完成,当输入的年龄合理时则正常使用,当输入的年龄不合理时(如-1,1000)则抛出自定义异常。我们可以通过扩展Exception类或RuntimeException类来实现自定义异常的创建。
创建自定义异常类,大体可以通过以下几个步骤来完成:
• 创建自定义异常类,但是该类需要继承Exception基类,如果自定义Runtime异常则需要继承RuntimeException基类。
• 定义构造方法。
• 使用异常。
接下来,通过案例来演示自定义异常的使用,如例8-14所示。
例8-14 Demo0814.java
1 package com.aaa.p080701;
2
3 // 自定义异常,继承Exception类
4 class CustomException extends Exception {
5 public CustomException() {
6 super();
7 }
8
9 public CustomException(String message) {
10 super(message);
11 }
12 }
13 public class Demo0814 {
14 public static void main(String[] args) {
15 try {
16 final int divide = divide(5, 0); // 调用divide方法
17 System.out.println(divide); // 打印结果
18 } catch (CustomException e) {
19 e.printStackTrace();
20 }
21 System.out.println("程序结束");
22 }
23
24 // 下面的方法实现了两个整数相除
25 public static int divide(int x, int y) {
26 if (y == 0) {
27 throw new CustomException("错误:除数不能为0!");
28 }
29 int result = x / y; // 定义一个变量result,记录两个数的商
30 return result; // 将结果返回
31 }
32 }
程序运行结果如下:
Error:(17, 23) java: 在相应的 try 语句主体中不能抛出异常错误com.aaa.p080701.CustomException
Error:(26, 25) java: 未报告的异常错误com.aaa.p080701.CustomException; 必须对其进行捕获或声明以便抛出
例8-14中,编译结果报错,提示的第1个异常是“未报告的异常错误con.aaa.p080701.CustomException,必须对其进行捕获或声明以便抛出”。原因在于,divide()方法中使用throw关键字进行了CustomException对象的抛出,而Exception类及其子类都是必检异常,因此必须对抛出的异常进行捕获或声明。提示的第2个异常是“在相应的try语句块中不能抛出异常错误com.aaa.CustomException”,这是因为系统不能确定try语句块中抛出的是什么类型的异常,但是如果我们将catch语句块后边的异常类型改为Exception时,是可以正常编译通过的,因为Exception类是所有异常类的基类。
对例8-14进行代码整改,第1种方法是在divide()方法中,使用try-catch对异常进行捕获处理;第2种方法是使用throws子句声明抛出CustomException异常。接下来,我们使用第2种方法将程序进行修改,如例8-15所示。
例8-15 Demo0815.java
1 Package com.aaa.p080701;
2
3 // 自定义异常,继承Exception类
4 class CustomException extends Exception {
5 public CustomException() {
6 super();
7 }
8 public CustomException(String message) {
9 super(message);
10 }
11 }
12 public class Demo0815{
13 public static void main(String[] args) {
14 try {
15 final int divide = divide(5, 0); // 调用divide方法
16 System.out.println(divide); // 打印结果
17 } catch (CustomException e) {
18 System.out.println(e.getMessage());
19 }
20 System.out.println("程序结束");
21 }
22
23 // 下面的方法实现了两个整数相除
24 public static int divide(int x, int y) throws CustomException {
25 if (y == 0) {
26 throw new CustomException("错误:除数不能为0!");
27 }
28 int result = x / y; // 定义一个变量result,记录两个数相除的结果
29 return result; // 将结果返回
30 }
31 }
程序运行结果如下:
错误:除数不能为0!
程序结束
例8-15中,自定义异常类CustomException来继承Exception类,divide()方法使用throw关键字抛出CustomException类的实例,并使用throws子句声明抛出该异常。从运行结果中发现,try-catch成功将自定义异常捕获。
最后需要明确指出,在使用自定义异常时,需要注意以下几点:
• 如果在当前抛出异常的方法中直接处理异常,可以使用try-catch语句块来进行异常的捕获和处理,也可以通过throws子句将异常抛出给方法调用者,做下一步的处理。
• 在调用异常方法的方法中捕获并处理异常。
8.7.2 异常丢失现象
通过前文的讲解可以看出,异常处理机制可以帮助开发人员解决很多的问题。但是,这并不代表它没有任何问题,其实Java的异常捕获也有瑕疵。当在try-finally中出现return语句或是进行手动异常抛出时,就会发现即使在try中捕捉到了异常,但是最后输出的错误信息也不是try中出现的异常信息,而是finally中出现的错误信息,这种现象被称为异常丢失。一旦产生异常丢失现象,那么开发人员可能就会被错误信息误导,从而进行错误的判断和处理。
接下来,通过案例来演示异常丢失现象及其对程序开发的干扰,如例8-16所示。
例8-16 Demo0816.java
1 package com.aaa.p080702;
2
3 public class Demo0816 {
4 void method1() throws MyException1 {
5 throw new MyException1(); // 手动抛出异常
6 }
7 void method2() throws MyException2 {
8 throw new MyException2(); // 手动抛出异常
9 }
10 public static void main(String[] args) {
11 Demo0807 demo = new Demo0807();
12 try {
13 try {
14 demo.method1();
15 } finally {
16 demo.method2();
17 }
18 } catch (Exception e) {
19 System.out.println(e);
20 }
21 }
22 }
23 class MyException1 extends Exception {
24 @Override
25 public String toString() {
26 return "异常丢失问题,MyException1类...";
27 }
28 }
29
30 class MyException2 extends Exception {
31 @Override
32 public String toString() {
33 return "异常丢失问题,MyException2类...";
34 }
35 }
程序运行结果如下:
异常丢失问题,MyException2类...
例8-16中,输出的结果为“异常丢失现象,MyException2类...”,main()方法中原本被抛出的MyException1并没有被捕获,这是由于Java虚拟机的机制造成的一点缺陷。产生这一问题的原因在于,finally中的代码一般是用于关闭资源的代码(如关闭文件、网络链接等),故而当程序执行至try-catch代码块的“边界处”时,便会转入finally代码块,而throw语句在字节码层面并非原语操作,所以当上面程序在执行到第5行时,Java虚拟机会将要抛出的异常的对象的引用存放到一个局部变量里,并将该变量存到方法栈的栈顶等待弹出,此时程序计数器指针指向finally内的代码,遇到下一个要抛出的异常时,该异常则顶替MyException1的对象引用所在位置,所以程序只会输出“异常丢失问题,MyException2类...”。
同理,在finally里也不要处理返回值。当返回值在finally语句块外返回时,由于throw并非原子语句,所以会用中间变量存储中间值,导致finally内处理的返回值并不能体现在返回的实际值上。
8.8 本章小结
• 异常是指程序运行期可能出现的非正常情况,非正常情况包括异常和错误,这些情况都将导致程序终止执行。
• 常见异常的类型:编译时异常、运行时异常。
• 异常的基类是Throwable类,派生出的子类分别是Exception类和Error类。
• 异常捕获结构由try语句块、catch语句块和finally语句块3个部分组成,其中try语句块存放的是可能产生异常的Java语句代码,catch语句块用来捕获try语句块中抛出的异常,finally语句块是最终执行的代码块,一定会被执行。try 语句块可以与catch语句块,finally语句块中的任意一个进行匹配,也可以两者都出现,但是fianlly语句块只允许出现一次。
• finally语句块中最好不要使用return语句,否则获取的结果既不是try语句块返回值也不是catch语句块返回值。
• 若某个方法可能会发生异常,但又不想在当前方法中处理这个异常,则可以使用throws、throw关键字在方法中抛出异常,由该方法的调用者进行处理。
• throw关键字用在方法体内,后边只有一个异常对象;throws关键字用在方法声明后边,可以有多个异常类,用逗号隔开。
• 异常捕获处理的方式有3种:分开捕获、嵌套捕获、联动捕获,建议使用联动捕获。
• 可以通过扩展Exception类或RuntimeException类来创建自定义的异常。
8.9 理论测试与实践练习
1.填空题
1.1常见异常类型有_______异常和_______异常。
1.2 Throwable类有两个子类:分别是_________类和__________类。
1.3 Java语言的异常捕获结构由_______、_______和________三个部分组成。
1.4在finally语句块中使用_______关键字会造成try中返回值丢失。
1.5 ________异常描述了Java程序运行时系统的内部错误,通常比较严重。
2.选择题
2.1 在异常处理中,哪个语句块可以有多个( )。
A.try子句 B.catch子句
C.finally子句 D.throws子句
2.2自定义异常类应该继承( )。
A.Error B.Exception
C.IOException D.SQLException
2.3 在异常类中 getMessage()的含义是( )。
A.返回异常信息描述字符串 B.输出异常信息的描述字符串
C.打印异常信息的描述字符串 D.输入异常信息描述的字符串
2.4 描述数组越界的异常名字是( )。
A.ArithmeticException B.NullPointException
C.EOFException D.ArrayIndexOutOfBoundException
2.5关于异常,下列说法不正确的是( ) 。
A.异常分为Error 和 Exception B.Throwable 是所有异常类的父类
C.在程序中无论是Error类型,还是Exception类型的异常,都可以捕获后进行异常处理 D.Exception 是RuntimeException 异常的父类
3.思考题
3.1请简述应该如何处理异常?
3.2请简述必检异常和免检异常?
3.3请简述Error和Exception的区别?
3.4请简述关键字throw和 throws区别?
4.编程题
4.1编写一个Exam类代表考试,提供输入成绩的方法。创建一个invalidResults异常类,如果输入成绩为负,则抛出一个invalidResults的对象。
4.2编写一个Diamond类代表菱形,在菱形中,菱形的四条边都相等。创建一个IllegalDiamondException异常类,在Diamond类的构造方法中,如果创建的菱形的边违反了这一规则,则抛出一个IllegalDiamondException对象。
- 点赞
- 收藏
- 关注作者
评论(0)