Java学习之旅第三季-3:异常处理机制之自定义异常
本小节首先列举了JDK中常见的运行时异常和受检异常类及其使用场景。然后详细讲解了Throwable类的核心属性和方法,包括detailMessage和cause属性,以及构造方法和常用实例方法。接着介绍了如何自定义异常类,包括继承体系、构造方法实现和业务场景应用示例。最后解释了异常链的概念,说明如何通过cause属性保存原始异常信息,便于调试和问题追踪。
目前我们使用的都是JDK中自带的异常类,这些异常是实际开发中是不能完全满足业务需求的,有一些业务上的异常情况也可以使用异常类表示,本小节先从JDK自带的异常类说起。
3.1 常见的内置异常
下表中是在学习Java时可能会见到的JDK自带的异常类:
| 异常类 | 异常类型 | 抛出的条件 |
|---|---|---|
| ArrayIndexOutOfBoundsException | 运行时异常 | 数组索引越界 |
| NullPointerException | 运行时异常 | 空指针异常,访问null的属性或方法引发此异常 |
| ClassCastException | 运行时异常 | 类向下转型出错 |
| IllegalArgumentException | 运行时异常 | 参数不合法时 |
| NumberFormatException | 运行时异常 | 数字格式不正确 |
| UnsupportedOperationException | 运行时异常 | 不支持的操作,属于集合框架的成员 |
| ClassNotFoundException | 受检异常 | 找不到类 |
| CloneNotSupportedException | 受检异常 | 克隆不支持 |
| IOException | 受检异常 | IO操作失败 |
| SQLException | 受检异常 | JDBC中执行SQL出错 |
| NoSuchFieldException | 受检异常 | 使用反射时,找不到属性 |
| NoSuchMethodException | 受检异常 | 使用反射时,找不到方法 |
| SocketException | 受检异常 | 创建于访问Socket出错 |
这些异常类大都是在JDK API中的方法抛出来的,开发人员更多的是要知道如何处理它们,很少直接抛出它们的实例。
3.2 异常类的方法
为了理解自定义异常,我首先介绍下异常层次中最上层的父类 Throwable 类。该类内部维护两个重要的私有属性:
- detailMessage:Throwable实例的描述信息
- cause:类型是Throwable,指代导致此 Throwable 实例产生的原因;cause 属性的初始值为 this(当前运行的 Throwable 实例),若其值为 null,表示此Throwable 实例并非由其他 Throwable 实例引发或者引发此 Throwable 实例的原因未知
下面的表只列出public修饰的构造方法和实例方法。
Throwable的构造方法有:
| 构造方法 | 描述 |
|---|---|
| Throwable( ) | 无参构造方法 |
| Throwable(String message) | 用于设置detailMessage属性的构造方法 |
| Throwable(Throwable cause) | 用于设置cause属性的构造方法 |
| Throwable(String message, Throwable cause) | 用于同时设置detailMessage和cause属性的构造方法 |
Throwable的常用实例方法列表如下:
| 方法 | 描述 |
|---|---|
| void printStackTrace() | 打印方法调用栈信息,定位错误有用 |
| String getMessage() | 获取 detailMessage 属性的值 |
| Throwable initCause(Throwable cause) | 为属性 cause 赋值,设置异常原因 |
| Throwable getCause() | 获取 cause 属性的值,如果cause是初始值this,则返回null |
Exception 类与 RuntimeException 仅仅提供了与 Throwable 类相同形式的构造方法,实现时仅仅使用super 调用了父类的构造方法,以 Exception 类的构造方法为例:
public Exception() {
super();
}
public Exception(String message) {
super(message);
}
public Exception(Throwable cause) {
super(cause);
}
public Exception(String message, Throwable cause) {
super(message, cause);
}
可以看到,异常类的实现相对比较简单,并不会提供太多的方法。
3.3 自定义异常
在JDK自带的异常无法满足业务需求时,开发人员可以在类层次上扩展自己的异常类,这些异常类与系统中的业务异常情况进行对应,当捕获到此异常时,说明出现了特定的业务异常情况。针对此情况可以有针对性的处理。
自定义异常的使用步骤如下:
1、声明类继承Exception或其子类,推荐继承 RuntimeException 或其子类
2、在异常类中声明构造方法,在其中显式调用父类的构造方法。这一步不是必须的,但是开发中一般要实现。
3、实现业务代码时,当某种异常条件成立时,使用 throw 抛出此异常的实例。
4、在调用业务方法时,使用catch捕获异常,并给出合适的处理方案
下面的自定义异常,用于在处理业务时库存不足时抛出。
/**
* 库存不足时抛出此异常
*
* @author 老谭
*/
public class UnderstockException extends RuntimeException {
public UnderstockException() {
}
public UnderstockException(String message) {
super(message);
}
public UnderstockException(Throwable cause) {
super(cause);
}
public UnderstockException(String message, Throwable cause) {
super(message, cause);
}
}
有了上面的异常类后,就可以在业务处理时使用它了。
/**
* 模拟查询某商品库存
*
* @return 库存
*/
public int getStock() {
return 10;
}
/**
* 模拟业务处理
*/
public void doProcess() {
int stock = getStock();
if (stock < 10) {
throw new UnderstockException("库存已不足,请及时补充");
}
}
上述代码中,当查询到的库存低于警戒值10 时,就认为是一个业务上的异常,此时可以使用 throw 抛出自定义异常的实例。在上一层调用此方法时,可以捕获此异常做出进一步的处理,比如给用户客户端一个友好的响应。由于每一个自定义异常都可以对应一个特定的业务异常场景,所以给用户的提示也非常的明确。
3.4 异常链
当某异常是由于其他异常(比如数据库操作异常)导致的,可以将导致异常的另一个异常作为原因,作为自定义异常实例中属性 cause 的值,这样形成了异常链,其目的是可以保留原始异常信息,便于调试和问题追踪。简单点说,所谓异常链,其实就是在异常实例中有一个属性cause保存了导致本异常的另一个异常实例。
为给 cause 属性赋值,可以使用异常类的构造方法传入,也可以使用 initCause 方法为 cause 属性赋值。要想获取到cause属性的值,则可以使用 getCause 方法即可。
还是先声明一个异常类,和上一个异常类类似:
/**
* 读取文件失败时抛出此异常
*
* @author 老谭
*/
public class FileReadException extends RuntimeException {
public FileReadException() {
}
public FileReadException(String message) {
super(message);
}
public FileReadException(Throwable cause) {
super(cause);
}
public FileReadException(String message, Throwable cause) {
super(message, cause);
}
}
接下来在一个工具类中声明读取文件内容的静态方法:
/**
* IO工具类
*
* @author 老谭
*/
public class IOUtil {
/**
* 根据指定的文件名读取文本文件中的第一行
*
* @param fileName 文件名
* @return 文本文件中的内容
*/
public static String readFile(String fileName) {
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
return reader.readLine();
} catch (FileNotFoundException e) {
// 将原始异常包装到自定义异常中,形成异常链
throw new FileReadException("文件读取失败:文件不存在", e);
} catch (IOException e) {
// 将原始异常包装到自定义异常中,形成异常链
throw new FileReadException("文件读取过程中发生错误", e);
}
}
}
声明的方法中,第15行的new FileReader(fileName)有受检异常FileNotFoundException要处理,第16行的 reader.readLine() 有IOException要处理。在使用catch分别捕获后,抛出了自定义差异,且将异常实例作为参数赋值到了父类中的 cause 属性。
测试代码如下:
try {
String content = IOUtil.readFile("example.txt");
System.out.println("文件内容:" + content);
} catch (FileReadException e) {
System.out.println("捕获到自定义异常:" + e.getMessage());
// 获取原始异常(cause)
Throwable originalException = e.getCause();
if (originalException != null) {
System.out.println("原始异常信息:" + originalException.getMessage());
}
// 打印完整的异常堆栈跟踪,包含整个异常链
e.printStackTrace();
}
通过捕获 FileReadException 异常,就可以使用其 getCause 方法获取到产生此异常的原因。这样在后续处理时更有针对性。当文件不存在时,运行此测试代码,结果如下:
捕获到自定义异常:文件读取失败:文件不存在
原始异常信息:example.txt (系统找不到指定的文件。)
com.laotan.article3.FileReadException: 文件读取失败:文件不存在
at com.laotan.article3.IOUtil.readFile(IOUtil.java:18)
at com.laotan.article3.IOUtil.main(IOUtil.java:27)
Caused by: java.io.FileNotFoundException: example.txt (系统找不到指定的文件。)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:185)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:139)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:109)
at java.base/java.io.FileReader.<init>(FileReader.java:60)
at com.laotan.article3.IOUtil.readFile(IOUtil.java:14)
... 1 more
可以看到第3行明确抛出了 FileReadException 异常,而在第6行的 Cause by 后就是导致FileReadException异常的另一个异常,这种异常链在实际开发中可能很长,特别是使用类似Spring这样的框架,我们定位错误时一般找到错误信息的最后才能比较清楚出错的根本原因是什么。
简而言之,这种方式的好处是既提供了高层的异常信息(我们的自定义异常信息),又保留了底层的原始异常信息,有助于开发者定位问题的根本原因。
如果没有形成异常链,比如抛出 FileReadException 时,没有传入第2个参数:
throw new FileReadException("文件读取失败:文件不存在");
最终的出错信息如下:
捕获到自定义异常:文件读取失败:文件不存在
com.laotan.article3.FileReadException: 文件读取失败:文件不存在
at com.laotan.article3.IOUtil.readFile(IOUtil.java:26)
at com.laotan.article3.IOUtil.main(IOUtil.java:35)
这几句信息比之前的就差多了。没有原始异常信息,开发中定位错误就更困难。
3.5 异常处理流程
常规情况下,try-catch-finally 结构会按照之前介绍的顺序往下执行,在try代码块中抛出异常,则程序进入异常捕获流程,及按照顺序从上到下依次执行catch中的对比,一旦异常类型匹配免责执行catch后的代码块,而无论是否有异常,finally都会执行。
但是有几种特殊殊情况需要注意:
-
不管在是什么位置,一旦执行System.exit() JVM会终止程序执行
-
try 代码块中有 return,finally中没有return;如果没有异常此时try 代码块中的 return 会临时存储该结果,待 finally 执行完成再返回,finally 中对 try 中的变量有更新,则根据返回的数据类型不同有不同的结果:
- 基本数据类型与String:不会影响try中return的结果
- 引用数据类:会影响try中return的结果
比如下面的test1方法,当没有出现异常时(参数 num 是奇数),在try代码块中的 return 会执行到,根据声明的结论,由于方法返回的是基本数据类型,所结果不受影响,仍为 1:
int test1(int num) {
int result = 1;
try {
if (num % 2 == 0) {
throw new Exception();
}
return result; // 没有异常时,临时存储result的值1
} catch (Exception e) {
result = 3;
}finally {
result = 5; // 不会对 result 重新赋值,回到第7行返回1
}
return result; // 没有异常时,这句不会执行到;出现异常时,这里会返回 5
}
但是下面的方法情况有所不同:
int[] test2(int num) {
int[] result = {1, 2, 3};
try {
if (num % 2 == 0) {
throw new Exception();
}
return result; // 没有异常时,临时存储result的值
} catch (Exception e) {
result[0] = 3;
} finally {
result[0] = 5; // 会对 result 中的元素重新赋值,回到第7行返回数组
}
return result; // 没有异常时,这句不会执行到;出现异常时,这里会返回finally更新过的数据
}
如果返回值类型是String,效果与第一个方法test1类似,究其原因是因为String的不可变导致的:
String test3(int num) {
String result = "Laotan";
try {
if (num % 2 == 0) {
throw new Exception();
}
return result; // 没有异常时,临时存储result的值
} catch (Exception e) {
result += "!";
} finally {
result += "!"; // 产生新的内存空间,对新的result赋值,回到第7行返回临时存储的字符串
}
return result; // 没有异常时,这句不会执行到;出现异常时,这里会返回finally更新过的数据
}
-
catch中有return,finally中没有return;效果与上一个类似。
-
当finally中有return的时候,try 或 catch中的return会失效,在执行完 finally 的 return 之后,就不会再执行 try 或 catch 中的 return。
int test(int num) {
int result = 1;
try {
if (num % 2 == 0) {
throw new Exception();
}
return result;
} catch (Exception e) {
return 4;
} finally {
return 5;
}
}
该方法无论是否有异常,永远返回 5。
对于上面这几种特殊情况,在实际开发中能避免应尽量避免,防止出现意想不到的bug。
3.6 小结
本小节首先列举了JDK中常见的运行时异常和受检异常类及其使用场景。然后详细讲解了Throwable类的核心属性和方法,包括detailMessage和cause属性,以及构造方法和常用实例方法。接着介绍了如何自定义异常类,包括继承体系、构造方法实现和业务场景应用示例。最后解释了异常链的概念,说明如何通过cause属性保存原始异常信息,便于调试和问题追踪。
更多推荐




所有评论(0)