本文是Java基础课程的第十课。主要介绍Java中的异常处理机制,包括异常与错误、常见异常、try…catch语句块、throw\throws关键字、自定义异常等内容
文章目录
一、Java中的异常与错误
1、什么是异常与错误
大多数情况下,程序运行过程中不可能一帆风顺,不可避免的会出现一些非正常的现象,比如用户输入了非法数据、程序要读取的文件并不存在、某一步运算出现除数是0的情况、访问的数组下标越界了、网络临时中断、甚至内存不够用而产生内存溢出等等。引起这些非正常现象的原因不一而足,有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。如果程序中出现了非正常的现象而没有得到及时的处理,程序可能会挂起、自动退出、甚至崩溃,程序的健壮性会大大降低。
在Java中,这些非正常现象可以分为异常和错误。
异常一般指在程序运行过程中,可以预料的非正常现象,异常一般是开发人员可控的,异常可以、也应当在程序中被捕获并进行相应的处理,以保证程序的健壮。
错误一般指在程序运行过程中,不可预料的非正常现象,错误对于程序来说往往是致命的,一般是开发人员很难处理、无法控制的,因此也不需要开发人员进行处理。
2、相关类继承关系
由于异常和错误总是难免的,良好的应用程序除了具备用户所要求的基本功能外,还应该具备准确定义并描述错误和异常,及预见并处理可能发生的各种异常的能力。Java定义了一系列用以描述错误和异常的类,并且引进了一套用以捕获、抛出、处理异常的机制。
在Java中,异常和错误都直接或间接继承自Throwable
类,Throwable
类有两个直接派生类,分别是Error
类和Exception
类,Error
类及其派生类用来描述错误,Exception
类及其派生类用来描述异常。下面是图示:
错误一般发生在严重故障时。虚拟机会捕获错误、实例化相应Error
类的派生类对象并抛出。通常发生错误的情况脱离开发人员的控制,也无法预料,所以在开发过程中通常不用刻意考虑。但开发人员应该认识一些可能会遇到的Error
类的派生类,方便在发生错误时定位、理解所发生的问题。
异常由Exception
类及其派生类来表示,Java中的异常也可以分成两部分,一部分是检查性异常,一部分是运行时异常。具体说明如下:
- 检查性异常:除了
RuntimeException
类及其派生类所代表的异常之外,其他Exception
类的派生类所代表的异常都是检查性异常。检查性异常在编译时不能简单的忽略,必须在源码中进行捕获处理,这是编译检查的一部分。检查性异常也被称作设计时异常。 - 运行时异常:
RuntimeException
类及其派生类所代表的异常都是运行时异常。运行时异常是可以通过开发人员的努力而避免的。与检查性异常相反的是,运行时异常可以在编译时忽略。运行时异常也被称作非检查性异常。
二、异常
前文中已经提到,Java中的异常分为检查性异常与运行时异常,检查性异常编译时不能忽略,强制要求开发人员在开发阶段捕获处理;运行时异常不强制要求在代码中捕获并处理,但开发人员应在编码过程中仔细完善代码逻辑,尽量避免运行时异常的发生。
下面是一个代码逻辑错误而导致运行时异常的示例:
package com.codeke.java.test;
public class Test {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
System.out.println("begin");
int result = num1 / num2;
System.out.println("end");
}
}
执行输出结果:
begin
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.codeke.java.test.Test.main(Test.java:8)
说明:
- 本例中,变量
num2
的值为0
,当num2
作为除数时,出现ArithmeticException
异常,程序退出,语句System.out.println("end")
不再执行。
1、JDK中常见的异常类
JDK中已经内置了很多异常类,开发人员在开发调式代码的过程中应该逐步认识并熟悉它们。
下面是一些JDK中常见的代表运行时异常的类:
异常类名 | 说明 |
---|---|
ArithmeticException | 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零" |
ArrayIndexOutOfBoundsException | 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引 |
ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时抛出的异常 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出该异常 |
IllegalArgumentException | 抛出的异常表明向方法传递了一个不合法或不正确的参数 |
IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常 |
IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常 |
StringIndexOutOfBoundsException | 此异常由字符串方法抛出,指示索引或者为负,或者超出字符串的大小 |
UnsupportedOperationException | 当不支持请求的操作时,抛出该异常 |
下面是一些JDK中常见的代表检查性异常的类:
异常类名 | 说明 |
---|---|
FileNotFoundException | 文件操作时,找不到文件,抛出该异常 |
ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常 |
IllegalAccessException | 拒绝访问一个类的时候,抛出该异常 |
NoSuchFieldException | 请求的字段不存在,抛出该异常 |
NoSuchMethodException | 请求的方法不存在,抛出该异常 |
InterruptedException | 一个线程被另一个线程中断,抛出该异常 |
2、捕获和处理异常
2.1、try…catch语句块
在Java代码中,使用try...catch
语句块可以捕获异常并进行处理,try...catch
语句块放在异常可能发生的地方,try...catch
语句块中的代码称为保护代码。使用try...catch
语句块的语法如下:
try {
// 程序代码
} catch (ExceptionName e) {
// catch 块
}
说明:
- 可能发生异常的程序代码放在
try
语句块中。 catch
关键字后面紧跟的()
中包含要捕获异常类型的声明,catch
语句块中包含的代码一般为对异常的处理。- 程序运行过程中,如果
try
语句块内的代码没有出现任何异常,后面的catch
语句块不执行;而当try
语句块内的代码发生一个异常时,try
语句块中的后续代码不再执行,系统会实例化一个该异常对应的异常类对象,后面的catch
语句块会被检查,如果该异常类对象 is acatch
关键字后面所声明异常类的对象,该对象会被传递到catch
语句块中,该catch
语句块中的代码将被执行。
下面是一个示例:
package com.codeke.java.test;
public class Test {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
System.out.println("begin");
// 使用try...catch 包裹可能发生异常的代码
try {
int result = num1 / num2;
} catch (ArithmeticException e) {
e.printStackTrace(); // 对异常的处理
}
// 后续代码仍将得到执行
System.out.println("end");
}
}
执行输出结果:
begin
java.lang.ArithmeticException: / by zero
at com.codeke.java.test.Test.main(Test.java:10)
end
说明:
- 本例中,用
try
语句块包裹了可能出现异常的代码int result = num1 / num2
。 - 当
try
语句块中出现算术运算异常时,系统实例化了一个ArithmeticException
类的对象,并检查该对象是否 is acatch
关键字后面所声明异常类型的对象,如果是,将对象传入catch
语句块。 - 在
catch
语句块中,异常类对象e
调用了printStackTrace()
方法,该方法可以向控制台打印异常信息。 - 异常被捕获处理后,程序没有退出,
catch
语句块之后的后续代码得以执行,本例中语句System.out.println("end")
被执行。
在异常发生时,所有的异常信息都被封装成为一个个异常类的对象,异常类从Throwable
类继承了一些常用的方法,用以获取异常信息,下面列出异常类常用的API:
方法 | 返回值类型 | 方法说明 |
---|---|---|
getMessage() | String | 返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了 |
getCause() | Throwable | 返回一个Throwable 对象代表异常原因 |
printStackTrace() | void | 打印toString()结果和栈层次到System.err,即错误输出流 |
getStackTrace() | StackTraceElement [] | 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底 |
fillInStackTrace() | Throwable | 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中 |
toString() | String | 使用getMessage()的结果返回类的串级名字 |
2.2、多重 catch
一个 try
语句块后面可以跟随多个 catch
语句块,用于对try
语句块中可能发生的多个异常进行捕获,这种情况也被称作多重捕获。
使用多重catch
语句块的语法如下:
try {
// 程序代码
} catch (ExceptionName1 e1){
// catch 块1
} catch (ExceptionName2 e2){
// catch 块2
} catch (ExceptionName3 e3){
// catch 块3
}
在有多重catch
语句块的情况下,如果try
语句块中发生异常,try
语句块中的后续代码不再执行,系统会实例化一个相应异常类型的对象,并检查从上往下第一个catch
关键字后面声明的异常类型,符合 is a 关系时,将对象传入catch
语句块,否则继续往下检查第二个catch
关键字后面声明的异常类型,直到找到对应的catch
语句块或通过所有的catch
语句块为止。
下面是一个针对多个可能发生的检查性异常,使用多重catch
的示例:
package com.codeke.java.test;
import java.io.*;
public class Test {
public static void main(String[] args) {
System.out.println("begin");
// 实例化file对象
File file = new File("D:\\test.txt");
try {
// 获取file对象的输入流
FileInputStream in = new FileInputStream(file);
// 读取输入流中的第一个字节
int i = in.read();
} catch (FileNotFoundException e) { // 第一个catch语句块
e.printStackTrace();
} catch (IOException e) { // 第二个catch语句块
e.printStackTrace();
}
System.out.println("end");
}
}
执行输出结果:
begin
java.io.FileNotFoundException: D:\test.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at com.codeke.java.test.Test.main(Test.java:12)
end
说明:
- 本例中,语句
FileInputStream in = new FileInputStream(file)
可能会发生FileNotFoundException
,语句int i = in.read()
可能发生IOException
,针对可能发生的这两个异常,使用了两个catch
语句块。 - 如果要捕获的异常类之间没有继承关系,各类的
catch
语句块顺序无关紧要,但当它们之间有继承关系时,应该将派生类的catch
语句块放在基类的catch
语句块之前。本例中,FileNotFoundException
为IOException
的派生类,故应该写在前面。
多重捕获也可以合并写在一个catch
语句块中,语法如下:
try {
// 程序代码
} catch (ExceptionName1 | ExceptionName2 [| ExceptionName3 ... | ExceptionNameN] e){
// catch 块
}
需要注意的是,这种写法仅限于要捕获的各异常类之间没有继承关系的情况。后续章节的代码中会出现这种情况,这里不再举例。
2.3、finally语句块
在try...catch
语句块后,可以使用finally
语句块,无论try
语句块中的代码是否发生异常,finally
语句块中的代码总是会被执行,也因此,finally
语句块中适合进行清理、回收资源等收尾善后性质的工作。
在try...catch
语句块后跟随finally
语句块需要使用finally
关键字,语法如下:
try {
// 程序代码
} catch (ExceptionName1 e1){
// catch 块1
} catch (ExceptionName2 e2){
// catch 块2
} finally {
// 必须执行的代码,适合收尾、善后等
}
下面是一个示例:
package com.codeke.java.test;
import java.io.*;
public class Test {
public static void main(String[] args) {
System.out.println("begin");
// 实例化file对象
File file = new File("D:\\test.txt");
FileInputStream in = null;
try {
// 获取file对象的输入流
in = new FileInputStream(file);
// 读取输入流中的第一个字节
int i = in.read();
} catch (FileNotFoundException e) { // 第一个catch语句块
e.printStackTrace();
} catch (IOException e) { // 第二个catch语句块
e.printStackTrace();
} finally { // finally语句块
try {
if (in != null) {
in.close(); // 关闭输入流,这个操作本身也可能发生IOException,要求强制检查
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("finally");
}
System.out.println("end");
}
}
执行输出结果:
begin
java.io.FileNotFoundException: D:\test.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at com.codeke.java.test.Test.main(Test.java:13)
finally
end
说明:
- 本例完善了上一示例,在获取输入输出流,进行完读写操作后,应当将输入输出流关闭,故在本例中,使用了
finally
语句块,无论是否发生异常,finally
语句块中的语句in.close()
都会将输入流关闭。 - 细心的开发者可能会考虑这样一个问题:如果不使用
finally
语句块,而是直接将finally
语句块中的语句放在catch
语句块外的后续代码中,无论try
语句块中是否发生异常,这些语句不是仍然会执行吗?finally
语句块又有什么使用的必要呢?事实上,考虑try
语句块或catch
语句块中有return
语句的情况,catch
语句块外的后续代码不一定能得到执行的机会,而就算try
语句块或catch
语句块中有return
语句,finally
语句块中的代码仍然会被执行,甚至,如果finally
语句块中也有return
语句时,会覆盖try
语句块或catch
语句块中的返回值,因此,使用finally
语句块来执行收尾善后工作是必要的,也是开发人员应该养成的一个良好的编码习惯。
3、抛出异常
3.1、throw关键字
通常,异常是自动抛出的。但开发人员也可以通过throw
关键字抛出异常。throw
语句抛出异常的语法格式如下:
throw new 异常类名([异常描述]);
下面是一个示例:
package com.codeke.java.test;
import java.util.Scanner;
/**
* 年满18周岁可报考驾校,如果年龄不满18周岁不允许包括驾校。
* 从键盘上输入年龄,如果年龄不足18岁,抛出异常
*/
public class Test {
public static void main(String[] args) {
System.out.println("请输入年龄:");
int age = new Scanner(System.in).nextInt();
validateAge(age);
System.out.println("年龄超过18岁,允许报考驾校");
}
/**
* 校验年龄是否不足18岁的方法
* @param age 要检验的年龄
*/
public static void validateAge(int age){
if(age < 18){
throw new RuntimeException("年龄不足18岁,不允许考驾校");
}
}
}
执行输出结果:
请输入年龄:
16
Exception in thread "main" java.lang.RuntimeException: 年龄不足18岁,不允许考驾校
at com.codeke.java.test.Test.validateAge(Test.java:24)
at com.codeke.java.test.Test.main(Test.java:14)
说明:
- 本例的
validateAge(int age)
方法中,当传入的参数age
不足18时,由开发人员实例化了一个运行时异常,并使用throw
关键字将该异常对象抛出。
3.2、throws关键字
对于需要捕获的异常(基本上是检查性异常),如果一个方法中没有捕获,调用该方法的主调方法应该捕获并处理该异常。为了明确某个方法不捕获某个异常,而让调用该方法的主调方法捕获异常,可以在方法声明的时候,使用throws
关键字抛出该类异常。在方法声明中抛出某类型异常的语法如下:
[修饰符] 返回值类型 方法名([参数列表]) throws 异常类型名 {
// 方法体
}
下面是一个示例:
package com.codeke.java.test;
import java.io.*;
public class Test {
public static void main(String[] args) {
try {
System.out.println("main start");
readFile();
System.out.println("main end");
} catch (IOException e) {
e.printStackTrace();
System.out.println("main catched");
}
System.out.println("over");
}
/**
* 读取文件
* @throws IOException IO异常
*/
public static void readFile() throws IOException {
File file = new File("D:\\test.txt");
// 获取file对象的输入流
FileInputStream in = new FileInputStream(file);
// 读取输入流中的第一个字节
int i = in.read();
}
}
执行输出结果:
main start
java.io.FileNotFoundException: D:\test.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at com.codeke.java.test.Test.readFile(Test.java:26)
at com.codeke.java.test.Test.main(Test.java:10)
main catched
over
说明:
- 本例中,
readFile()
方法中可能出现FileNotFoundException
和IOException
,但在readFile()
方法并不想直接捕获处理这些异常,故可以在方法声明时使用throws
关键字抛出异常给主调方法(由于FileNotFoundException
是IOException
的派生类,故抛出IOException
即可),此时,在主调方法中仍然需要捕获并处理被调方法抛出的异常。
4、自定义异常
系统定义的异常不能代表应用程序中所有的异常,有时开发人员需要声明自定义异常。声明自定义异常非常简单,将系统定义的异常类作为基类,声明派生类即可。一般在声明自定义异常时,会选择继承Exception
类或RuntimeException
类。从Exception
类继承的自定义异常是检查性异常,在应用程序中必须使用try...catch
语句块捕获并处理;不过自定义异常一般是可控的异常,大部分情况下不需要捕获,因此让自定义异常直接继承自RuntimeException
类是开发人员更多情况下的选择。
下面是一个示例:
InputException
类的源码:
package com.codeke.java.test;
/**
* 输入异常
*/
public class InputException extends RuntimeException {
public InputException(String message) {
super(message);
}
}
测试类Test
类的源码:
package com.codeke.java.test;
import java.util.Scanner;
/**
* 校验输入的姓名不为空且长度是否不小于6位
*/
public class Test {
public static void main(String[] args) {
System.out.println("请输入用户名");
String name = new Scanner(System.in).next();
validateName(name);
}
/**
* 校验姓名是否存在并且长度不小于6位
* @param name 要校验的姓名
*/
public static void validateName(String name) {
if (name == null || name.length() < 6) {
throw new InputException("用户名必须填写,长度不小于6位");
}
}
}
执行输出结果:
请输入用户名:
tom
Exception in thread "main" com.codeke.java.test.InputException: 用户名必须填写,长度不小于6位
at com.codeke.java.test.Test.validateName(Test.java:22)
at com.codeke.java.test.Test.main(Test.java:13)
说明:
- 本例中,创建了一个自定义异常
InputException
,它继承自RuntimeException
类,故是一个运行时异常,不强制要求使用try...catch
语句块捕获并处理。 - 在本例的
validateName(String name)
方法中,当传入的参数name
为空或长度小于6位时,实例化了一个自定义异常,即InputException
类的对象,并使用throw
关键字将该异常对象抛出。