Java 异常处理

Java 异常处理

Java 异常处理

Java 异常处理通过 5 个关键字控制:trycatchfinallythrowthrows

处理异常的一般形式:

  • try 不可单独出现,允许 try…finally 这样的组合,catch 语句可以有多个,finally 语句只能有一个;
  • ExceptionType_1、ExceptionType_2 是想要处理的异常类型,只有相匹配的类型才会被对应的 catch 捕获;
  • 如果 ExceptionType_1 和 ExceptionType_2 存在继承关系,要将子类的 catch 放在前面,否则会被其父类一并捕获;
  • 异常控制的流程和 if…else 一样,如果在 try 中检测到异常,会按照从上到下的顺序依次匹配所有的 catch 块,如果符合,则执行匹配到的 catch 语句,并停止向下匹配,执行完 catch 之后跳转到 finally 语句(如果存在的话),之后执行 finally 之后的正常语句;
  • Java SE 7 之后,一个 catch 块可以匹配多个异常,使用|管道符号分割,如catch (E1 | E2 e) { statement },这种情况下,变量 e 自动变为 final 常量。

异常类型
所有异常类型都是 java.lang.Throwable 的子类,而 Throwable 有两大直接派生类:ErrorException
Java Throwable 继承体系

浅蓝色的ErrorRuntimeException非检查异常,即不要求一定要进行处理;而粉红色的则为检查异常,即此类异常必须进行处理,否则编译不通过。

其实很好理解:

  • Error,即运行时错误,既然是错误,肯定是无法在编码上进行修复的,因此不要妄图去 catch 它,而应重新审视你的程序,或者是修改 JVM 配置(如内存溢出,说明分配的内存不够用)。
  • RuntimeException,即运行时异常,这种异常一般都是可以通过改进程序逻辑去避免的,比如数组越界异常,应该在操作数组前,检查参数的正确性。
  • Exception,即一般性异常,或者称为检查异常,这种异常才是我们应该去 catch 或 throws 的。如果你能够妥善的处理它,则进行 catch,否则请将它抛给调用者,即 throws。

对于一般性异常(后面直接简称异常),想谈谈自己的个人理解。
首先,我们应该改变对异常的看法,很多人都认为异常就是错误,是一种非正常情况。其实不然,异常并不等同于错误,如果是的话,那就不会出现什么 Error、RuntimeException、Exception 了。异常其实是在方法调用过程中很有可能出现的一种正常情况,或者说,异常其实是函数返回值的另一种表现形式

为什么这么说呢?举个例子,在 java.util.concurrent.Future 接口中,有这么一个方法,它声明了三个异常:
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
分别为:InterruptedException即中断异常、ExecutionException即执行异常、TimeoutException即超时异常。

这个方法是用来获取一个异步调用结果的,该方法会阻塞,我们可以指定阻塞时间(即超时时间),它们分别代表:
InterruptedException:调用线程在阻塞期间被另一个线程中断
ExecutionException:该异步任务在执行的过程中发生了异常
TimeoutException:等待了指定时长后,异步任务仍未完成

为什么要声明这么多异常呢?可以将它们都去除吗?当然可以,这三个异常都可以不要它,如V get(long timeout, TimeUnit, unit),然后在方法内部的逻辑是:不论是发生超时、还是线程被中断、或者是任务执行异常,都返回 null 值。

但是这样的话,调用此方法后,你获取了一个 null 引用,你还能知道这个任务的具体执行情况吗?是发生了超时呢?还是当前线程被中断了呢?还是该任务执行失败了呢?因此,我们需要这些异常的存在,因为它们都是检查异常,因此,你不能忽略它们,必须根据不同的异常来进行不同的程序逻辑。

异常其实是函数返回值的另一种表现形式的含义所在,有些方法调用情况是无法通过返回值区分的,必须通过异常来区分。

Throwable 类

异常处理原则
1、避免过大的 try 块,不把不会出现异常的代码放到 try 块里面,尽量保持一个 try 块对应一个或多个异常;
2、细化异常的类型,不要不管什么类型的异常都写成 Excetpion、RuntimeException、Throwable;
3、catch 块尽量保持一个块捕获一类异常,不要忽略捕获的异常,捕获到后要么处理,要么重新抛出新类型的异常;
4、不要把自己能处理的异常抛给别人;
5、不要用try...catch参与控制程序流程,异常控制的根本目的是处理程序的非正常情况。

异常转型
异常转型实际上就是捕获到异常后,将异常以新的类型的异常再抛出,这样做一般为了异常的信息更直观!

异常默认处理方式
所有未被 try…catch 处理的异常都将被 Java 运行时系统的默认处理程序捕获;默认处理程序会调用 Throwable.printStackTrace() 打印异常信息,然后结束程序。

自定义异常类型
如果 Java 中的内置异常类型不能满足你的需要,你可以创建一个属于你自己的异常类型;自定义异常类型可以继承于 Exception、RuntimeException 等基类。

finally 的执行顺序

finally 块的作用和 C++ 的 RAII 很相似,只不过 Java 中没有析构函数,因此使用 finally 块来替代。

throw 抛出异常
我们除了可以处理别人抛出的异常之外,还可以自己显式的抛出一个异常,语法:throw exceptionObj;exceptionObj 必须是 Throwable 类及其派生类的实例,其它类型都是不被允许的。

抛出异常的例子:

throws 异常声明
throws 表示方法可能会抛出异常的声明,如果添加了 throws 子句,表示该方法即将抛出异常,异常的处理交由它的调用者,至于调用者任何处理则不是它的责任范围内的了。

所以如果一个方法会有异常发生时,但是又不想处理或者没有能力处理,就可以使用 throws 来抛出这个异常给调用者去处理。throws语句位于方法声明的后面,如:void func() throws ClassCastException { ... }throws可以声明多个异常,多个异常之间使用逗号隔开。

检查异常必须在 throws 中声明,非检查异常可不必声明。throws 语句的目的:明确告知调用者可能抛出哪些异常;JavaDoc 也可对该异常进行说明使 API 更加友善。

例子:

异常的性能影响

创建一个异常对象的时空开销比创建一个普通的对象要大的多;因为创建一个异常对象往往需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的;构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。因此,异常处理过程中的主要开销就是创建异常抛出和捕获异常都没有什么性能影响(因为 try、catch 语句本质为 goto 语句)。

那么 try…catch 和 for/while 循环一起使用呢?比如下面两个例子:

两者在功能上有很明显的区别:
前者:只要在 for 循环中有任何一个异常,循环就会终止;
后者:如果转换失败则打印”bad_format”,并不会直接终止循环。

那它们的性能区别呢?
性能无非就是看空间消耗时间消耗,一开始的时候我想当然得觉得 try…catch 重复执行了这么多次肯定比只执行了一次跑得肯定慢,空间消耗肯定更大;但是实际上并不是,具体的讨论贴地址为:Stack Overflow 讨论帖地址,讨论的结果是:在没有抛出异常的情况下,性能完全没区别

Java 异常处理的原理,大致流程:
1、类会跟随一张异常表(exception table),每一个 try…catch 都会在这个表里添加行记录,每一个记录都有 4 个信息(try…catch 的开始地址,结束地址,异常的处理起始位,异常类名称);
2、当代码在运行时抛出了异常时,首先拿着抛出位置到异常表中查找是否可以被 catch,如果可以则跑到异常处理的起始位置开始处理,如果没有找到则原地 return,并且 copy 异常的引用给父调用方,接着看父调用的异常表,以此类推。

综合上面来看可以得出结论:
1、异常如果没发生,也就不会去查表,也就是说你写不写 try…catch 也就是有没有这个异常表的问题,如果没有发生异常,写 try…catch 对性能是没有消耗的,所以不会让程序跑得更慢;
2、try 的范围大小其实就是异常表中两个值(开始地址和结束地址)的差异而已,也是不会影响性能的。

当然 try…catch 绝对不是在什么地方都可以乱加的!要在适当的场合使用适当的特性!

assert 断言功能

断言的概念
断言的目的是为了标示与验证程序开发者预期的结果,当程序运行到断言的位置时,对应的断言应该为真;若断言不为真时,程序会中止运行(抛出 AssertionError 错误),并打印错误消息;断言可以在运行时从代码中完全删除,所以对代码的运行速度没有影响。

断言的使用
1) assert bool_expression:bool_expression 为布尔表达式,当它报告 false 时,将抛出 AssertionError
2) assert bool_expression : expression:expression 是其构造函数的参数,即new AssertionError(expression)

如果布尔表达式的值为 true,则表示程序的运行符合开发者预期的结果,这时候有没有断言都不影响程序的运行;如果布尔表达式的值为 false,则表示程序的运行存在问题,不符合预期,需要进行修改,断言失败将抛出 AssertionError 错误,AssertionError 继承自 Error 类,不应该被 catch 处理。

java.lang.AssertionError 错误

断言是 JDK1.4 引入的一个功能,并且 Java 是默认关闭断言的,需要使用选项-ea显式启用断言。

例子:

断言的使用场景
断言通常用于验证方法中的内部逻辑,包括:
1) 内在不变式
2) 控制流程不变式
3) 后置条件和类不变式
不推荐用于公有方法内的前置条件的检查。

断言中的布尔表达式不应改变上下文环境,关闭断言后会导致程序运行错乱;
断言的主要用途是用于开发阶段的测试,尽量发现可能的 bug,并进行修复;
不应该将断言用于函数入口的参数检查,并且在正式运行的时候需要关闭断言。

运行时屏蔽断言
运行时要允许断言,可以用java –enableassertionsjava -ea
运行时要屏蔽断言,可以用java –disableassertionsjava -da