Java 异常处理机制,一篇带你搞清楚

在Oracle公司编写的《Java使用指南》中关于"异常"的描述占据了相当大的篇幅,可以说对于"异常"的处理是开发人员必须熟练掌握的技能。毕竟,程序员大部分工作不是在改BUG,就是在改BUG的路上。

异常的概念:

异常是程序中的一些错误,错误可能来源于程序本身错误,也可能是用户误操作所致,还可能是物理因素引起的。由但并不是所有的错误都是异常,并且错误有时候是可以避免的。

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

要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:

  1. 可检查性异常:除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常;
    检查异常是编译器要求必须处置的异常,这些异常在编译时不能被简单地忽略。当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过。
    在正确的程序运行过程中,很容易出现这类异常,且在一定程度上这种异常的发生是可以预测的,情理可容的。最具代表的检查性异常是用户错误或问题引起的异常,例如:要打开一个不存在文件时,一个异常就发生了。
  2. 运行时异常: 包括RuntimeException及其子类和Error;
    与检查性异常相反,运行时异常为编译器不要求强制处理的异常,因为它并不是语法错误,只有在编译时才会出现,在业务逻辑上需要程序员特别注意。
  3. 错误: 错误不是异常,而是脱离程序员控制的问题;
    错误在代码中通常被忽略。
    例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

异常的结构体系:

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。
在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。
异常的结构体系
如图所示:

我们通常所说的"异常类"指的是Exception,它继承自Throwable;Error也继承自Throwable,但是Error通常不属于程序处理的范畴,它表示不希望被程序捕获或者是程序无法处理的错误。

Exception又可以分为非可查性异常(Unchecked Exception)和可查性异常(Checked Exception),它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。

下面详细讲述下Exception和Error之间的区别和联系:

  • Error:Error类对象由 Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。
    这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
  • Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;
    而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

异常处理原则

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

  • 抛出异常的思路:要理解抛出异常,首先要明白什么是异常情形(exception
    condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。
    抛出异常后,会有几件事随之发生。首先,是像创建普通的java对象一样将使用new在堆上创建一个异常对象;然后,当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。
    要注意的是,必须对异常进行处理或者抛出,坚决不能不处理。如果程序到了最后一级,一定要try…catch…处理,坚决不能再抛出给JVM,否则程序会挂掉。
  • 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception
    handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

异常处理语法

Java异常处理涉及到五个关键字,分别是:try、catch、finally、throw、throws。

  1. try – catch 关键字

使用 try 和 catch 关键字可以捕获异常,代码块放在异常可能发生的地方。
匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。

try{
   // 程序代码:用于监听异常,当try语句块内发生异常时,异常就被抛出;
}catch(Exception e){
   // catch块:用于捕获异常,捕获try语句块中发生的异常;
}

这里讲一下“多重捕获”的情况:
很多情况下,由单个的代码段可能引起多个异常。处理这种情况,我们需要定义两个或者更多的catch子句,每个子句捕获一种类型的异常,当异常被引发时,每个catch子句被依次检查,第一个匹配异常类型的子句执行,当一个catch子句执行以后,其他的子句将被旁路。

try{ 
	// 程序代码 
}catch(异常类型1 异常的变量名1){ 
	// 程序代码 
}catch(异常类型2 异常的变量名2){ 
	// 程序代码 
}catch(异常类型3 异常的变量名3){ 
	// 程序代码 
}catch(Exception e){ 
	// 程序代码 
}

如图,通常做法是:Exception >异常类型3>异常类型2>异常类型1
对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。通常,在最后写Exception,来确保异常肯定会被捕获。

  1. finally 关键字

try-catch体还可以定义一个finally模块,finally模块通常用于资源的回收。
finally体的特点是不论程序有无异常产生都能得到执行,所以我们可以把try块里资源的回收等操作交给它处理(如数据库连接、网络连接和磁盘文件),以便保证其内容一定能得到执行,看下面的实例:

try{
  // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}finally{
  // 程序代码
}

注意:只有finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。

  1. throw、throws 关键字

如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明,意为抛出异常给父集。但有一点要明白:必须有一级代码需要对异常进行处理,不能一直抛出异常,最终交给JVM处理,因为JVM对检查性异常的处理只有一个方法 – 让系统挂掉。

import java.io.*;
public class className{
	public void deposit(double amount) throws RemoteException{
		// Method implementation
		throw new RemoteException();
	}
	//Remainder of class definition
}

再说说throw/throws的关系:

  • throws是用在方法的定义处,用以声明方法体内部可能会产生的异常类型。如果代码中其他方法调用了这样有异常声明的方法,必须对其进行处理或者接着抛出,坚决不能不处理;
  • throw的用法是抛出异常对象,它本身就是一个抛出异常的动作,通常在方法体内进行调用,通常throw和throws是成对出现的。

异常输出方法

当异常捕获完成以后,需要在控制台告知程序员程序到底怎么了。下面的列表是 Throwable 类的主要方法:

方法名 方法介绍
public String getMessage() 返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。
public Throwable getCause() 返回一个Throwable 对象代表异常原因。
public String toString() 使用getMessage()的结果返回类的串级名字。
public void printStackTrace() 打印toString()结果和栈层次到System.err,即错误输出流。
public StackTraceElement [] getStackTrace() 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。
public Throwable fillInStackTrace() 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。

通常,小编习惯用两种方法来查看异常情况,分别是:e.getMessage() 和 e.printStackTrace() 。

自定义异常

除了系统定义的各种"异常"之外,有时候根据程序的需要,开发人员需要定义自己的"异常",也就是"自定义异常"。"自定义异常"有三个原则:

  • 自己定义:"自定义异常"需要继承自Exception类或其子类;
  • 自己抛出:"自定义异常"需要根据情况自己抛出异常对象;
  • 自己处理:需要程序自己来处理"自定义异常情况";

通常来说,自定义异常继承这两个类即可,原则如下:

  • 如果希望写一个检查性异常类,则需要继承 Exception 类,此类异常必须处理;
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类,可以不处理;

我们以继承 Exception 为例:

/* 文件名 : MyException .java */
public class MyException extends Exception {
	public MyException() {
		super("我是自定义异常");
	}
}

/* 文件名 : TestMyException .java */
public class TestMyException {
	public void test() throws MyException {
		throw new MyException();
	}
	public static void main(String[] args){
		try{
			TestMyException test = new TestMyException();
			test.test();
		}catch(MyException e){
			System.out.println(e)
		}
	}
}

如果自定义异常继承了 RuntimeException ,处理起来会更简单,但是缺乏了 Exception 类的严谨:

  • 如果在函数内容抛出该异常,函数上不用声明,编译一样通过;
  • 如果在该函数上声明了该异常,调用者可以不用运行处理,编译一样通过;

常见异常

最后,我列举一些常见的内置异常,供大家参考学习:

  • 非检查性异常类:(RuntimeException)
异常 描述
ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException 试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
IllegalMonitorStateException 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。
IllegalStateException 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。
IllegalThreadStateException 线程没有处于请求操作所要求的适当状态时抛出的异常。
IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
NullPointerException 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯。
StringIndexOutOfBoundsException 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。
UnsupportedOperationException 当不支持请求的操作时,抛出该异常。
  • 检查性异常类:(IOException)
异常 描述
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常。
CloneNotSupportedException 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。
IllegalAccessException 拒绝访问一个类的时候,抛出该异常。
InstantiationException 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。
InterruptedException 一个线程被另一个线程中断,抛出该异常。
NoSuchFieldException 请求的变量不存在
NoSuchMethodException 请求的方法不存在

小结

  1. 在 try/catch 后面添加 finally 块并非强制性要求的;
  2. try 代码后不能既没 catch 块也没 finally 块,反之,catch和finally 也不能独立于 try 存在;
  3. try, catch, finally 块之间不能添加任何代码;
  4. 如果catch块中有return,那么finally语句要在return之前执行;
  5. 必须对异常进行处理或者抛出,坚决不能不处理。如果程序到了最后一层,一定要try…catch…处理,坚决不能再抛出给JVM,否则程序会挂掉;
  6. 自定义类可以继承 Exception类 和 RuntimeException类,区别是继承 Exception类后被定义为可检查异常,必须处理,程序上更加严谨。

更多精彩,请关注我的"今日头条号":Java云笔记
随时随地,让你拥有最新,最便捷的掌上云服务