Java异常处理详解

🤣 子曾曰过:所有的炒冷饭都是温故而知新。

当初写Java的时候,时不时会加上这么一段,管它对不对,反正是好的:

1
2
3
4
5
6
7
try {
// 可能会发生异常的程序代码
} catch (Exception ex) {
// 捕获并处理try抛出的异常类型ex
} finally {
// 无论是否发生异常,都将执行的语句块
}

就这样无头无脑的用,但这背后的东西并没有过多的了解和学习,现在把Java异常好好总结一番 ✍️

🐳​​🐳🐳​ Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字:

  • try – 用于监听。try后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码,当try语句块内发生异常时,异常就被抛出。【监控区域】
  • catch – 用于捕获异常。catch后对应异常类型和一个代码块,用于处理try块发生对应类型的异常。【异常处理程序】
  • finally – 用于清理资源,finally语句块总是会被执行。 多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。【使用finally进行清理】
  • throw – 用于抛出一个实际的异常。throw可以单独作为语句使用,抛出一个具体的异常对象。【抛出异常】
  • throws --用在方法签名中,用于声明该方法可能抛出的异常。【异常说明】

异常的概念

基本概念

《Thinking in Java 4th》一书第12章“通过异常处理错误”介绍到:

Java的基本理念就是“结构不佳的代码不能运行”

发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前。然而,编译期间并不能找到所有错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。

异常处理是Java中唯一正式的错误报告机制,并且通过编译器强制执行。

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。

异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。

异常处理机制

Java的异常处理本质上是抛出异常捕获异常

基本异常

异常情形是指阻止当前方法或者作用域继续执行的问题。

把异常情形与普通问题相区分开来,普通问题是指:在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。

举一个简单的栗子🌰

ExceptionExample

异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。它允许我们(如果没有其他手段)强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到稳定状态。

❤ 异常参数

与使用Java中的其他对象一样,异常对象也是用new在堆上创建,这就涉及到存储空间的分配和构造器的调用。

所有标准异常类都有两个构造器:

1️⃣ 默认无参构造函数;

2️⃣ 接受字符串作为参数,以便能把相关信息放入异常对象的构造器:

throw new Exception("message");

在用new创建了异常对象后,此对象的引用将传给throw 。尽管返回的异常对象其类型通常与方法设计的返回类型不同(ps:通常,异常对象中仅有的信息就是异常类型),但从效果看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制。另外还能用抛出异常的方式从当前作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。

捕获异常

首先要理解监控区域的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

❤ try块

如果在方法内部抛出了异常(或在方法内部调用其他方法抛了异常),这个方法将在抛出异常的过程中结束。要不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。

1
2
3
try{
// Code that might generate exceptions
}

❤ catch块

抛出异常必须在某处得到处理。这个“某处”的地点就是异常处理程序。异常处理程序紧跟try块之后,以关键字catch 表示:

1
2
3
4
5
6
7
8
9
10
try{
// Code that might generate exceptions
}catch(Type1 t1){
//Handle exceptions of Type1
}catch(Type2 t2){
//Handle exceptions of Type2
}catch(Type3 t3){
//Handle exceptions of Type3
}
...

每个catch子句(异常处理程序)看起来就像是接收一个且仅接收一个特殊类型的参数的方法。

异常处理程序必须紧跟在try块之后。当异常抛出时,异常处理机制负责搜寻参数与异常类型相匹配的第一个处理程序,注意每个catch子句是被依次检查。然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则处理程序的查找过程结束。注意,只有匹配的catch子句才能得到执行

🐬 注意:异常的匹配

抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它认为异常将得到处理,然后就不再继续查找。

编写多重catch语句块的顺序问题:先小后大,即先子类后父类

MultipleCatch

🐬 注意:多重catch块

Java通过异常类描述异常类型。对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。

终止与恢复

异常处理理论上有两种模型:

  • 终止模型:假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行,一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
  • 恢复模型:异常处理程序的工作是修正错误,然后尝试调用出问题的方法,并认为第二次能成功。对于该模型,通常希望异常被处理之后能继续执行程序。如果想用Java实现类似恢复的行为,那在遇见错误时就不能抛出异常,而是调用方法来修正该错误。或者,把try块放在while循环里,不断进入try块,直到满意的结果。

长久以来,尽管程序员们使用的操作系统支持恢复模型的异常处理,但是他们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。

❤ throw

到目前为止,我们只是获取了被Java运行时系统引发的异常。然而,我们还可以用throw语句抛出明确的异常。Throw的语法形式如下:

1
throw ThrowableInstance;

这里的ThrowableInstance一定是Throwable类类型或者Throwable子类类型的一个对象。

有两种方法可以获取Throwable对象:在catch子句中使用参数或者使用new操作符创建。

程序执行完throw语句之后立即停止;throw后面的任何语句不被执行,最邻近的try块用来检查它是否含有一个与异常类型匹配的catch语句。如果发现了匹配的块,控制转向该语句;如果没有发现,次包围的try块来检查,以此类推。如果没有发现匹配的catch块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestThrow{
static void proc(){
try{
throw new NullPointerException("demo");
}catch(NullPointerException e){
System.out.println("Caught inside proc");
throw e;
}
}
public static void main(String [] args){
try{
proc();
}catch(NullPointerException e){
System.out.println("Recaught: "+e);
}
}
}

结果:

1
2
3
D:\java>java TestThrow
Caught inside proc
Recaught: java.lang.NullPointerException: demo

该程序两次处理相同的错误,首先,main()方法设立了一个异常关系然后调用proc()。proc()方法设立了另一个异常处理关系并且立即抛出一个NullPointerException实例,NullPointerExceptionmain()中被再次捕获。

异常说明 throws

如果一个方法可以导致一个异常但不处理它,它必须指定这种行为以使方法的调用者可以保护它们自己而不发生异常。Java提供了异常说明 来告知程序员某个方法可能会抛出的异常类型。它属于方法声明的一部分,紧跟在形式参数列表之后。这对于检查异常 (除了ErrorRuntimeException及它们子类以外类型的所有异常)是必要的。

throws子句的方法声明的通用形式:

1
2
3
public void info() throws Exception{
//body of method
}

Exception 是该方法可能引发的所有的异常。也可以是异常列表,中间以逗号隔开。

🐬 注意,这里区分throw和throws

  • throws是声明一个方法可能抛出的所有异常信息。在方法声明中,如果添加了throws子句,表示该方法即将抛出异常,异常的处理交由它的调用者,至于调用者如何处理则不是它的责任范围内的了。所以如果一个方法会有异常发生时,但是又不想处理或者没有能力处理,就使用throws吧!
  • throw是抛出一个具体的异常类型。它不可以单独使用,要么与try…catch配套使用捕获异常,要么与throws配套使用声明抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestThrow {
public static void main(String[] args){
try{
//调用带throws声明的方法,必须显式捕获该异常。
//否则,必须在main方法中再次声明抛出
throwChecked(-3);
}catch (Exception e){
System.out.println(e.getMessage());
}
//调用抛出Runtime异常的方法既可以显式捕获该异常,也可不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception {
if (a > 0) {
//自行抛出Exception异常
//该代码必须处于try块里,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a){
if (a > 0){
//自行抛出RuntimeException异常,既可以显式捕获该异常
//也可完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}

🐬 注意:异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常说明列出的那些异常。

异常限制对构造器不起作用。

finally

对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。这通常适用于内存回收之外的情况(因为回收有垃圾回收器完成)。为了达到这个效果,可以在异常处理程序(catch块)后面加上finally 子句。

1
2
3
4
5
6
7
8
9
10
11
12
try{
// Code that might generate exceptions
//that might throw A,B or C
}catch(A a1){
//Handle exceptions of A
}catch(B b1){
//Handle exceptions of B
}catch(C c1){
//Handle exceptions of C
}finally{
//Activities that happen every time
}

无论异常是否被抛出,finally子句总能被执行。

❤ 如何使用finally

Java有垃圾回收机制,内存释放不是问题。当要把除内存之外的资源恢复到它们初始状态时,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
BufferedReader br = null ;
try {
//在出现异常的地方,就终止执行代码
//然后进入到catch
br = new BufferedReader(new FileReader("C:\\in.txt"));
//如果catch语句不止一个,则进入匹配异常那个catch
//实用方面的话,catch语句中用Exception是最简单的
//可以采用从小到大捕获异常的思想,性能可能好些,但是较复杂
} catch (Exception e) {
e.printStackTrace();
} finally {
//不管有没有异常都会执行
//用来释放资源,连接等
if (br != null) {
try {
br.close();
System.out.println("Buffer流成功关闭");
} catch (Exception e) {
e.printStackTrace();
}
}
}

甚至在异常没有被当前的异常处理程序捕获的情况下,异常处理机制也会再跳到更高一层的异常处理程序之前,执行finally子句。

CodeExample

❤ 在return中使用finally

因为finally自己总是会执行的。所以,在一个方法中,可以从多个点返回,并且保证重要的清理工作仍旧会执行。

CodeExample2

异常链

异常链顾名思义就是将异常发生的原因一个传一个串起来,即把底层的异常信息传给上层,这样逐层抛出。 Java API文档中给出了一个简单的模型:

1
2
3
4
5
try {
lowLevelOp();
} catch (LowLevelException le) {
throw (HighLevelException) new HighLevelException().initCause(le);
}

当程序捕获到了一个底层异常,在处理部分选择了继续抛出一个更高级别的新异常给此方法的调用者。 这样异常的原因就会逐层传递。这样,位于高层的异常递归调用getCause()方法,就可以遍历各层的异常原因。 这就是Java异常链的原理。

在JDK1.4以前,程序员必须自己编写代码来保存原始异常的信息。现在所有Throwable的子类在构造器中都可以接受一个cause(因由)对象作为参数。这个cause就用来表示原始异常。但,在Throwable子类中,只有三种基本的异常类提供了带cause参数的构造器。分别是:Error(用于Java虚拟机报告系统错误)、Exception以及RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause方法,而不是构造器。

异常链的实际应用很少,发生异常时候逐层上抛不是个好注意, 上层拿到这些异常又能奈之何?而且异常逐层上抛会消耗大量资源, 因为要保存一个完整的异常链信息.

自定义异常

Java提供的异常体系不可能预见所有希望加以报告的错误,所以可以自己自定义异常类来表示程序中可能遇到的特定问题。

自定义异常类,必须从已有的异常类(Throwable或其子类)继承,最好选择意思相近的异常类继承。建立新的异常类最简单的方法就是让编译器为你产生默认构造器,然后它将自动调用基类的默认构造器。也可以为异常类定义一个接受字符串参数的构造器。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/** 自定义异常 继承Exception类 **/
public class MyException extends Exception{
public MyException(){}
public MyException(String message){
super(message);//调用其基类构造器,接受一个字符串作为参数
}
}
public class Test {
public void display(int i) throws MyException{
if(i == 0){
throw new MyException("该值不能为0.......");
}else{
System.out.println( i / 2);
}
}
public static void main(String[] args) {
Test test = new Test();
try {
test.display(0);
System.out.println("---------------------");
} catch (MyException e) {
e.printStackTrace();//信息(从方法调用出直到异常抛出处的方法调用序列)将被输出到标准错误流
}
}
}

异常层次结构

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。参见API

Throwable 类的主要方法:

ThrowableMethod

在Java API中已经定义了许多异常类,这些异常类分为两大类:

  • 错误 java.lang.Error :其子类表示了Java虚拟机的异常。它表示不希望被程序捕获或者是程序无法处理的错误。Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程。
  • 异常 java.lang.Exception :其子类表示了程序运行中的异常。它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。

Exception的子类还可以分为两类:

  • java.lang.RuntimeException :其子类表示了运行中的异常,该异常可以不被catch,编译器也能通过。该子类表示的异常,均应该在源代码中避免,比如数组范围的超出等等。
  • 非RuntimeException :其子类表示了程序中不可避免的异常,如文件不存在的 FileNotFoundException 异常,该异常必须被 catch 掉或者是在函数头处声明。

Java异常又可以分为不受检查异常(Unchecked Exception)和 检查异常(Checked Exception

  • 检查异常:在正确的程序运行过程中,很容易出现的、情理可容的异常状况,在一定程度上这种异常的发生是可以预测的,并且一旦发生该种异常,就必须采取某种方式进行处理。

⚠️ 除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过。

  • 不受检查异常包括RuntimeException及其子类和Error

⚠️ 不受检查异常 为编译器不要求强制处理的异常,检查异常则是编译器要求必须处置的异常。

Java异常层次结构图如下所示:

ExceptionStructure

相关链接

ExceptionSolve

文章目录
  1. 1. 异常的概念
    1. 1.1. 基本概念
  2. 2. 异常处理机制
    1. 2.1. 基本异常
      1. 2.1.1. ❤ 异常参数
    2. 2.2. 捕获异常
      1. 2.2.1. ❤ try块
      2. 2.2.2. ❤ catch块
        1. 2.2.2.1. 终止与恢复
      3. 2.2.3. ❤ throw
    3. 2.3. 异常说明 throws
    4. 2.4. finally
      1. 2.4.1. ❤ 如何使用finally
      2. 2.4.2. ❤ 在return中使用finally
    5. 2.5. 异常链
    6. 2.6. 自定义异常
  3. 3. 异常层次结构
  4. 4. 相关链接
|