Throwable
类是Java语言中所有错误和异常的类。 只有作为此类(或其一个子类)的实例的对象由Java虚拟机抛出,或者可以由Javathrow
语句抛出。1.使用 try和catch处理异常;认识异常的继承架构;了解 throw、throws的使用时机;
2.运用 finally关闭资源以及使用自动关闭资源语法;认识AutoCloseable接口。
目录
一、异常处理与继承架构
1.使用try…catch
2.异常继承架构
3.抓还是抛
4.继承与异常
5.异常的设计与思考
6.堆栈追踪
7.断言assert
二、异常与资源管理
1.使用finally
2.自动尝试关闭资源
3.AutoCloseable接口
1.使用try…catch
在实际编程中,我们总会有一些意想不到的状况而引发程序错误,这是合理和允许的。在Java中,错误都会以对象方式呈现为java.lang.Throwable的各种子类实例,只要我们捕捉到这些对象就可以针对错误进行处理。比如进行程序修复、进行日志记录或是以某种形式反馈给用户。
使用try……catch语法,JVM就会尝试执行try区块中的程序代码,如果发生错误,就会跳离错误的发生点,去匹配catch括号中声明的类型,是否符合被抛出的错误对象类型,若是则执行区块代码。
如果抛出了Throwable,而程序没有任何catch捕捉到错误对象,最后由JVM捕捉到,那JVM基本处理就是显示错误信息并中断程序。
2.异常继承架构
在编写程序中,有一些程序如果不编写try…catch语句,就会编译错误,编译程序就一定要求你在程序中明确处理错误。那接下来我们就需要了解一下这个错误包装对象的继承架构,哪些异常一定要求处理。
首先这些错误对象都是可抛的,所以设计错误对象继承自java.lang.Throwable类,这个类会定义取得错误信息、堆栈追踪(Stack Trace)等方法,它有两个子类:java.lang.Error与java.lang.Exception。
其中,Error或其子类实例,代表系统的严重错误,比如硬件层面、JVM错误或者内存不足等等,虽然可以用异常语法处理但这类错误是Java应用程序无力解决的,只能任其到JVM或者留下日志信息。
而如果是程序设计产生的错误,则可以使用Exception或其子类实例表现,通常称错误处理为异常处理(Exception Handling)。
从语法与继承架构来看,Exception还分为java.lang.RuntimeException异常和其他异常。
对于其他这些异常都要必须明确使用try…catch语句加以处理,或是用throws声明抛出异常,否则即编译失败。即IO~、SQL~红色区域这一类或其子对象称为受检异常(Checked Exception),受编译程序检查。
而属于蓝色区域的RuntimeException这一类或其子对象,代表API设计者实现某方法,因用户设定的某些条件引发的错误(比如数组越界、比如用户输入格式错误),客户端应该在调用方法前自行检查,称为非受检异常(UnChecked Exception),在执行时期异常,编译程序不管。
下面贴了一张JDK8版本的Throwable继承架构图。和JDK7不同的是,ClassNotFoundException不直接继承Exception而是继承新增的Reflective~。
架构顺序:了解异常的继承架构后,有一些细节我们需要注意,比如在使用try…catch捕捉对象的时候,如果父类异常对象在子类异常对象前被捕捉了,那么子类异常对象的区块将永远不会被执行,编译程序也会报错。解决方案是更改顺序。
多重捕捉:可以捕获多个异常进行处理,程序会一一进行匹配异常类型;如果有数个类型的catch区块在做相同的事情,这样区块内容重复的问题我们可以需要优化,从JDK7之后,可以使用多重捕捉(Multi-catch)语法,每个异常写在同一个catch括号中用 | 隔开,也要注意架构顺序。
3.抓还是抛
前面介绍了异常继承的架构,我们明确了什么异常必须去处理,什么异常是不强制的。
但如果在方法设计流程中发生异常,我们在设计过程中没有充足的信息去处理该怎么办呢?
有的人会选择在catch代码区块里写空,有的人会很随意的应付显示一个错误信息,这些都是对应用程序造成伤害的操作,切记①不可写空,否则异常信息空白,用户会一无所知,而且会很难寻找错误根源。②不可对异常做不适当的处理或显示不正确的异常信息,这样会误导出错方向。
我们可以选择抛出异常。意思是目前环境信息不足以使用try…catch处理异常,可由客户端自行处理。
抛出异常使用throws声明,必须声明异常类型或者父类型,告诉编译程序才能通过。
抛受检异常 → 表示你认为调用方法的客户端有能力且应该处理异常,throws部分会是API操作接口的一部分,即声明方法时直接抛;
抛非受检异常 → 表示程序设计不当引发的漏洞,可能是调用方法的时机有问题,应该提醒客户端修改程序逻辑后再来调用,避免引发错误。实际处理中,可以在catch代码区块中先处理部分事项,再抛也是可以的,使用throw(注意区别),即在执行时抛。
※ 如果catch到的两个异常类型都属于一个父类,且处理异常的流程相同,可以直接catch父类。在JDK7以后,编译程序会更精准判断(More-Precise-Rethrow),JDK7之前编译出错。
4.继承与异常
使用继承时,如果父类某个方法声明throws某些异常,子类重写方法时
可以:
- 不声明throws任何异常
- throws父类该方法中声明的某些异常
- throws父类该方法中声明异常的子类
但是不可以:
- throws父类方法中未声明的其他异常
- throws父类方法中声明异常的父类
5.异常的设计与思考
异常设计的本意是,程序错误发生时,能够有明显的方式通知给客户端,并让它修正错误。
为了让异常更能表现出应用程序特有的错误信息,我们可以为应用程序自定义专属的异常类别。即定义一个异常类继承相关的异常子类,通常建议继承自Exception或其子类(自定义受检异常);按具体情况而定,也可以继承自RuntimeException(非受检异常)。
目前来看,Java是唯一采用受检异常机制的语言。两个目的:一是文件化,受检异常声明会是API操作接口的一部分,我们查阅API文件都可以知晓;二是提供编译程序信息,让编译程序在编译时期就检查出异常。
然而有些问题根本无力处理,我们就只能抛。抛看似没什么问题,但如果这个方法是发生在应用程序的底层被调用,我们如何把异常显示出来呢?只能一层一层的抛,往上浮现。(抛的问题)
采用受检异常的做法这是Java的设计,有时确实比较麻烦,尤其是随着程序规模的增大,维护也会造成困难。像很多开发人员在设计链接库的时候,干脆选择完全使用非受检异常,包括Spring、Hibernate框架都是这样;先如今我们也可以考虑将异常进行演化,比如一开始设计为受检异常,当随着一层一层往上声明抛出的时候(代表无力解决,这是设计漏洞),我们就可以将其演化为非受检异常。
不管是受检异常或不是,我们都应当思考,这个异常是客户端可以处理的异常,还是本身没有准备好前置条件就调用的逻辑设计漏洞?
6.堆栈追踪
在多重方法的调用下,异常发生点是不太好确定的,我们若想知道其根源,以及多重方法调用下的堆栈传播,可以利用异常对象自动收集的堆栈追踪(Stack Trace)来获取相关信息。
直接调用printStackTrace( )方法即可在控制台显示。信息包括异常类型,顶层是根源,以下是调用方法的顺序。当然也可以使用其他方法将堆栈追踪信息以指定方式输出至目的地。
还有其他方法,比如getStackTrace( )可以返回StackTraceElement数组,可以按索引获取个别的堆栈追踪元素进行处理,如getClassName()、getFileName()、getLineNumber()、getMothodName()。
注意一点的是,即使使用throw重抛异常,异常的追踪堆栈起点还是异常发生的根源,而不是重抛异常的地方;想要让重抛异常作起点,使用fillInStackTrace()可重新装填异常堆栈,以重抛异常的位置作为起点,并返回Throwable对象,所以我们抛的时候还要注意强制转换成异常类型。
7.断言assert
有时候在需求或设计的时候就可以确认,在程序执行的某一个时间点,或某个情况下,一定是处于或不处于什么样的状态,比如变量值一定是多少,如果违反了就是错误。这种提前判断好了预期结果是一种断言(Assertion),断言的结果一定是成立或者不成立。在JDK4之后提供这个断言语法,有两种形式,后面接表达式或者书写表达式的细节。
assert boolean_expression; assert boolean_expression:detail_expression;
使用场景其一,是用作前置条件检查,在程序上线后就不再需要检查。
使用场景其二,是一定不发生的某种情况,比如switch-case中default情况永远不会发生,只会有我们自定义的枚举常数情况发生,那么default就可以使用assert判定。
注意一点,断言是一种程序自检,可当作可执行的注释。而不能当作if判断式使用,不属于程序执行流程的一部分。
※ 断言的设计是让错的程序看得出错,这是由防御式程序设计(Defensive Programming)来实现的速错(Fail Fast)概念。貌似名声不太好。
1.使用finally
如果因错误而抛出异常,导致程序的执行流程中断,那么抛出异常之后的程序就不会再执行,但考虑一点,我们所创建的对象或是开启的一些相关资源却并没有关闭,那我们该怎么设计呢。
无论如何,我们都需要最后一定执行关闭资源的动作,比如一些数据流或是Scanner控制台都有close方法,写在异常的后面就不会执行了。我们可以用try…catch语法结合finally来达到目的。无论try区块有无发生异常,finally区块里的代码一定会被执行。我们可以进行检查参考对象是否为null,不是则进行关闭。
※ 即使return写在finally区块的前面,finally区块也将优先执行。
2.自动尝试关闭资源
因为关闭资源的流程都是一样的,检查然后调用方法关闭。在JDK7之后,新增了尝试关闭资源(Try-With-Resources)语法。可以把想要尝试自动关闭资源的对象编写在try之后的括号中try( ){ ;}如果不需要写catch处理异常可以不用写。finally也不用再写。
这种语法是编译程序蜜糖,实际上也是try、catch、finally。如果代码没写catch,反编译之后也会发现编译程序会尝试catch所有错误,其中addSuppressed()方式是JDK7在Throwable类中新添加的,可将第二个异常记录在第一个异常之中。也可以搭配catch同时显示堆栈追踪信息。
这个语法仅协助你关闭资源,而不是处理异常。使用之后不可再重复编写关闭资源的程序代码。
3.AutoCloseable接口
注意一点,JDK7的尝试关闭资源语法可套用的对象,必须操作java.lang.AutoCloseable接口,这个可以通过API查询到,也就是说如果我们自定义的对象也想用这个语法,也必须要操作这个接口。
AutoCloseable也是JDK7新增的接口,只定义了close()这一种方法。
尝试关闭资源语法也可以同时关闭两个以上的对象资源,用分号隔开。try括号里,编写越后面的对象会越早被关闭。反编译后,我们可以观察到,每一个AutoCloseable对象都使用一套try、catch、finally。而且try括号里最后面的对象的try语法会嵌套最前面的,即try包含着前一对象的try、catch、finally。