前言
本篇文章是对Java异常体系相关内容及部分注意事项的的讲解。
一. 认识异常
在每个人的生命历程中,或多或少都会遇到生病或受伤的情况,比如:皮肤擦伤、感冒、发烧、患上某些传染病等等。不管“病情”严重与否,这些都可以算作人体出现的“异常”。
在Java中,一个程序执行过程中发生的不正常行为就称为异常。
在Java的异常体系中,可以分为 Exception(异常) 和 Error(错误)两大类, 它们都继承自 Throwable 这个顶级父类,而 Exception 又可以分为 Checked Exception(受查异常) 和 Unchecked Exception(非受查异常)两大类,每一类异常又划分出各类详细的异常。(如下图)
在上图异常体系中:
- Error 指的是 JVM 无法解决的严重问题,它通常由环境问题或严重的系统错误引起。这种错误通常不建议进行捕获。
- Exception 指的是程序本身可以处理的异常,通常可以使用 try-catch 的方式对异常进行捕获,并在发生时针对具体的异常做出不同的处理。
- 其中Exception 的子类 Checked Exception(受查异常,也称作编译时异常)必须在编译期间进行处理(try-catch 或 将异常作为声明抛出);Unchecked Exception (非受查异常,也称作运行时异常)通过在程序运行期间才会被发现,因此则无需进行显式处理,在异常真正发生时会交给 JVM 处理。(如下图)
二. 异常的处理
1. 异常处理的两种思维方式
首先,我们必要要清楚一个事实:对于编程初学者或经验不足的编程人员来说,写出有 BUG(异常)的程序是一个正常的现象,我们不能因此害怕异常,而应该在每次异常出现时了解异常出现在代码层面的根本原因,学习与积累解决异常的方法。这样长久以往,我们终能写出一个 BUG 很少的程序。(甚至没有 BUG。当然了,这只是对自己的激励,现实情况中再牛的程序也会出现 BUG)
咳咳,回到正文。
对于不可预期的异常,通常有两种解决的思维方式:(1)“防御式” 编程(2)“事后认错型”编程。
-
“防御式”编程:在一个完整的事件流程中,针对每一步骤的执行结果进行检查,若其中某个步骤的执行结果出现错误则立即终止执行流程,以免后续得到不可预期的错误结果。(如下图)
从上图可以发现:“防御式”编程会严格检查每一步的执行结果,确保错误在第一时间被发现并处理。但上述伪代码的执行流程存在一个明显的问题:正常的执行流程和错误处理流程混合在一起,导致代码的简洁性和可读性比较差。 -
“事后认错型”编程:让可能出现异常的流程先执行,等到异常真正出现再统一对异常进行处理。(推荐!)
从上图不难发现:正常流程和错误处理流程分开,可以让程序员清晰直观地了解执行流程,增强代码的可读性。
2. 处理异常
在Java中,对异常的处理主要涉及到 5 个关键字:throw
、throws
、try
、catch
、finally
。
- throw:该关键字用于在代码中主动抛出一个指定的异常。这个异常需要是 Throwable 类或其子类的实例。它的作用是在程序出现错误时,将对应的错误信息通知给调用者。
它的使用示例如下:
// 该方法的作用:为数组指定下标设置 val
public boolean setElement(int[] nums, int val, int index) {
if (nums == null) {
throw new NullPointerException();
}
if (index < 0 || index >= nums.length) {
throw new IndexOutOfBoundsException();
}
nums[index] = val;
return true;
}
- throws:该关键字用于方法声明时提示该方法可能会抛出的异常类型。调用方必须对这些可能抛出的异常进行处理,若调用方不想对异常做出处理,依然可以使用 throws 关键字抛出这些异常,将异常处理交给上一层的调用者, throws 最多能将处理权传递到 main()方法,即交给 JVM 处理。
它的使用示例如下:
public static void readFile() throws IOException{
// 对一个文件的内容进行读取
}
public static void databaseOption() throws SQLException {
// 查询或修改数据库数据
}
注意:
throws用于声明可能抛出的受查异常。因为对于非受查异常来说,只有在程序运行期间才会被发现,因此即使一个自定义方法在方法签名中声明了非受查异常,方法的调用方也不需要对该异常进行显式处理。
- try:该关键字用于定义一个代码块,其中包含可能抛出异常的语句。try 关键字用于处理受查异常时不能单独使用,后面通常需要跟着多个 catch 块或一个 finally 块。(catch 和 finally 至少需要一个,也可以同时使用)
- catch:该关键字定义的代码块用于处理捕获到的异常。
- finally:无论 try代码块 中是否抛出异常,finally 关键字定义的代码块最终都会执行,它主要用于释放被打开的资源,防止发生异常时后续关闭资源的语句不能被及时调用,从而造成系统资源泄露的问题。例如: IO读写流、数据库连接等。
它的使用示例如下:
public static void main(String[] args) {
try {
readFile();
databaseOption();
} catch (IOException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 进行收尾工作及释放已打开的资源
}
}
public static void readFile() throws IOException {
// 对一个文件的内容进行读取
}
public static void databaseOption() throws SQLException {
// 查询或修改数据库数据
}
运行结果如下:
注意:finally中的代码块不一定会执行。这与上面 finally 关键字的说明并不冲突,比如以下三种特殊情况:
(1)在 finally 代码块执行前,Java虚拟机被提前终止运行(如下图)(2)程序所在的线程死亡(3)CPU被关闭
三. 使用 try-resource-with 代替 try-catch-finally
try-resource-with 是 Java7 引入的一种异常处理机制,用于自动关闭资源。它的使用初心与 try-catch-finally 一致,都是为了在程序发生异常时及时关闭已打开的资源,但使用 try-resource-with可以使写出的代码更加简洁清晰,并且能够有效减少因忘记关闭资源导致的系统资源泄露问题。
try-resource-with 的适用范围(资源的定义):任何实现 java.lang.AutoCloseable
或 java.io.Closeable
的对象。
try-resource-with 的执行顺序:无论是否发生异常,都会在 try 代码块执行结束后自动调用关闭资源的操作。
它的使用示例如下:
public static void main(String[] args) {
// 使用 try 将要打开的资源用括号“包裹”起来
try (FileReader reader = new FileReader("test.txt")) {
// 执行文件读写操作
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
注意:try 关键字后的括号可以包含多个需要使用的资源,它们之间使用 ; 进行分隔。(如下)
public static void main(String[] args) {
try (FileReader reader = new FileReader("test.txt");
FileWriter writer = new FileWriter("text2.txt")) {
// 执行文件读写操作
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章可能存在许多不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。