前言
在java中使用异常处理有什么优势?相信这是许多从C语言转型过来的同学的一个问题,这个问题也曾一直困扰着我,希望通过这篇文章能让大家开始对这个问题有一个新的开端,开始了解和使用异常。
大家知道在C语言中一般出现异常是通过返回特殊的值来判断的,比如-1或者NULL等。所以在写java程序的时候也会惯性的使用这种方式,对何时使用异常非常的模糊,偶尔发现使用某个函数必须捕获异常的时候才会使用异常处理机制。下面就通过几个例子让大家了解一下什么情况下需要使用异常,使用异常又有什么好处呢?
使用异常的优势
优势1:隔离错误处理代码和常规代码
先上一个把文件读入内存的伪代码
readFile {
open the file; //打开一个文件
determine its size; //判断文件大小
allocate that much memory; //分配内存
read the file into memory; //将文件读入内存
close the file; //关闭文件
}
如果我们按照通过判断返回值的方式来处理异常的话代码应该是这样的
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) { //读取文件失败
errorCode = -1;
}
} else { //内存不够分配
errorCode = -2;
}
} else { //不能确定文件的大小
errorCode = -3;
}
close the file;
if (theFileClosed && errorCode == 0) {
errorCode == 0;
} else { //文件不能关闭
errorCode = errorCode and -4;
}
} else { //文件不能打开
errorCode = -5;
}
return errorCode;
}
有那么多的错误检测、报告和返回语句,使得业务逻辑完全淹没在其中了。如果没有考虑周到,在排查错误和修改逻辑的时候将会是一个大麻烦。
那么如果使用异常的方式呢?
readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) { //文件打开异常
doSomething;
} catch (sizeDeterminationFailed) { //读取文件大小异常
doSomething;
} catch (memoryAllocationFailed) { //内存分配异常
doSomething;
} catch (readFailed) { //读取文件异常
doSomething;
} catch (fileCloseFailed) { //关闭文件异常
doSomething;
}
}
这样是不是业务逻辑就非常的清晰了呢?非常有利于后期的开发和维护。
优势2:在调用栈中向上传播错误
假如在主程序中,在一系列嵌套的调用方法中,readFile方法是第四个方法: method1调用method2,然后调用method3,最后调用readFile。先看一下文代码。
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}
假设method1是唯一关心在readFile内发生了什么错误的方法。传统的错误通知技术强制method1和method2传播readFile返回的错误代码,直到错误代码最后传到method1——而method1是唯一需要返回码的方法。
method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}
method2和method3对这个错误是不关心的,但是由于method1关心这个错误,所以这两个方法必须将错误一层一层的传递出去,如果使用异常是不是会有所不同呢?
method1 {
try {
call method2;
} catch (exception e) {
doErrorProcessing;
}
}
method2 throws exception {
call method3;
}
method3 throws exception {
call readFile;
}
正如伪代码所示,中间方法通过在throws从句中进行声明,可以抛掉任何检查异常,让关心错误的方法去检测和处理错误。这也是异常处理的一个重要的原则。
优势3:归类和区分错误类型
因为程序抛出的所有异常都是对象,类是有层次结构的,所以对异常进行归类或分组是非常自然的结果。在Java平台中有一个例子,一组相关异常被定义在java.io包——IOException及其子类。IOException是最通用的,它代表了我们在执行I/O相关操作的时候发生的任何错误类型。它的继承者代表了更加细分的错误。例如,FileNotFoundException表示一个文件不能再磁盘中定位到。
一个方法可以写一个具体的处理器,能处理一个十分具体的异常。比如使用FileNotFoundException来处理文件未被发现的异常:
catch (FileNotFoundException e) {
...
}
一个方法也可以用更通用的处理器捕获处理具体的异常。例如,为了捕获所有的I/O异常,不管具体的类型是什么,只要给异常处理器指定一个IOException参数就行:
catch (IOException e) {
...
}
你甚至可以构建一个可以处理所有异常的异常处理器:
catch (Exception e) {
...
}
然而在大多数情况下,你希望异常处理器越具体越好。理由是在你决定最佳的恢复策略之前,你首先要知道错误的类型。事实上,如果不捕获具体的错误,这个处理器就必须要容纳任何可能性。太通用的异常处理器可能会让代码更容易出错,因为它们会捕获和处理程序员意料之外的异常,这样就超出处理器的能力范围了。就像前面说过的,你可以创建一组异常,然后采用通用处理的风格。或者你可以使用具体的异常类型来区分不同的异常,然后用精确的风格处理异常。
Java异常处理的心得
异常处理机制的三个基本目的
- 归类处理不同的异常
- 提供足够的信息方便调试
- 让主流程代码保持整洁
使用异常的三个原则
-
具体明确
-
提早抛出
-
延迟捕获
Java异常处理实践原则
-
使用异常,而不使用返回码
-
利用运行时异常设定方法使用规则
-
设定方法的使用规则,遇到不合法的使用方式时,立刻抛出一个运行时异常。这样既不会让主流程代码变复杂,也不会制造不必要的BUG。
-
-
消除运行时异常
-
当你的程序发生运行时异常,通常都是因为你使用别人的方法的方式不正确,采取修改代码的方式,而不是新增一个异常流程
-
-
正确处理检查异常
-
让可以处理这个异常的方法去处理。衡量的标准就是在你这个方法写一个处理器,这个处理器能不能做到1和2的那两个要求,如果不能,就往上抛。如果你不能知道所有用户的所有需求,你通常就做不到那两个要求。
-
有必要的时候可以通过链式异常包装一下,再抛出。
-
最终的处理器一定要做到1和2那两个要求。
-
-
使主流程代码保持整洁
-
使用try-with-resources
-
尽量处理最具体的异常
-
设计自己的异常类型要遵循的原则
-
确定需要创建自己的异常类型的场景
-
为你的接口方法的使用规则创建一组运行时异常。
-
包装别人的检查异常的时候,一定也要用检查异常。这样异常才能传递给上层方法处理。
-
设计一组有层次结构的异常,而不是设计一堆零零散散的异常。
-
区分清楚异常发生的原因,然后决定你的异常是检查异常还是运行时异常。
-
模块内部不需要处理自己定义的异常。
-
创建自己的异常类型的场景
-
你是否有一个异常类型,Java平台里面的异常类型都不能描述它?
-
它是否可以帮助用户区分这个异常类型是你的还是其他供应商的?
-
你的代码中是否会抛出多种相关的异常?
-
如果你用了别人的异常类型,用户是否有权限访问这些异常?也就是,你的代码包是不是独立的?
常见的五类异常
-
和资源(Resource)交互,见图⑤处。这里资源的范围很广,比如进程外部的数据库,文件,SOA服务,其他各种中间件;进程内的类,方法,线程……都算是资源。
-
给进程内的其他方法(User Method)提供服务,见图②处。
-
依赖进程内的其他方法(Server Method),见图③处。包括Java平台提供的方法和其他第三方供应方提供的方法。
-
和系统环境交互,见图⑧处。系统环境可能是直接环境——JVM,也可能是间接环境——操作系统或硬件等。
-
给外部实体提供服务,见图①处。这种外部实体一般会通过容器(或其他类似的机制)和你的方法进行交互。所以,可以归为②,不予探讨。
五类异常的处理方法
1.当和资源交互时,常常会因为资源不可用而发生异常,比如发生找不到文件、数据库连接错误、找不到类、找不到方法……等等状况。有可能是直接产生的,见图⑤处;有可能是间接产生的,比如图⑥处发生异常,Server Method把异常抛给Your Method,图③处就间接发生了异常。一般来说,你写的方法间接发生这类异常的可能性比直接发生要大得多,因为直接产生这类异常的方法在Java平台中已经提供了。对于这类异常,通常有以下几个特点:
-
问题来自外部,你的方法本身的正常流程逻辑没问题。
-
这类异常通常是暂时的,过段时间就可用了。最终用户通常也可以接受暂时的等待或采取替补方案。
-
你的程序的其他功能还可以用。
处理方法:
-
返回到一种安全状态,并能够让用户执行一些其他的命令(比如支付宝支付失败时返回一个弹出框,说明余额不足等原因,让用户重新选择其他支付渠道);
-
或者允许用户保存所有操作的结果,并以适当的方式终止程序(比如保存填了一半的表单)。
然后,你应该协调各方,促进资源恢复可用,消除异常。
2.当给用户方法(User Method )提供服务时,用户可能会传入一些不合法的数据(或者其他不恰当的使用方法),进而对程序的正常流程造成破坏。你的方法应该检查每一个输入数据,如果发现不合法的数据,马上阻止执行流程,并通知用户方法。
当调用服务方法(Server Method )时,有可能会发生两类异常。
-
一类是你的使用方法不正确,导致服务中止;
-
一类是服务方法出了异常,然后传递给你的方法。
处理方法:
如果是第一种异常,你应该检查并修改你的方法逻辑,消除BUG。
对于第二类异常,你要么写一个处理器处理,要么继续传递给上层方法。
3.当和系统环境交互时,有可能因为JVM参数设置不当,有可能因为程序产生了大量不必要的对象,也有可能因为硬故障(操作系统或硬件出了问题),导致整个程序不可用。当这类异常发生时,最终用户没法选择其他替代方案,操作到一半的数据会全部丢失。你的方法对这类异常一般没什么办法,既不能通过修改主流程逻辑来消除,也不能通过增加异常处理器来处理。所以通常你的方法对这类异常不需要做任何处理。但是你必须检查进程内的所有程序和系统环境是否正常,然后协调各方,修改BUG或恢复环境。
总结
Java的异常都是发生在方法内,所以研究Java异常,要以你设计的方法为中心。我们以“你的方法 ”为中心,总结一下处理办法:
- 当服务方法告诉“你的方法 ”的主流程逻辑有问题时,就要及时修复BUG来消除异常;
- 当用户方法非法使用“你的方法”时,应该直接中止主流程,并通知用户方法,强迫用户方法使用正确的方式,防止问题蔓延;
- 当服务方法传递一个异常给“你的方法”时,你要判断“你的方法”是否合适处理这个异常,如果不合适,传递给上层方法,如果合适,写一个异常处理器处理这个异常。
- 当系统环境出了问题,“你的方法”什么也做不了。
刚才以“你的方法”为中心,总结了在“你的方法”内部的处理办法。现在以“你”为中心,总结一下方法外部的处理方法:
- 当资源不可用的时候,你应该协调各方,恢复资源;
- 当发生系统故障时,你应该协调各方,恢复系统。
异常的层次
Java异常处理机制类层级结构图
所有的功能都在Throwable类里面实现了,子类只需要直接继承或间接继承它,并且加上需要的构造方法就行(一般而言,第一第二个构造方法是必须的,也可以全部加上),而且构造方法通常只需要一行代码:super(...),也就是说只要调用父类的构造方法就行了。
Java把异常分为三类(Error,Checked Exception,RuntimeException),只是在语法层面上有不同的标记而已。它们自身拥有的功能一样,运行时系统处理它们的方式也是一样的(你也可以捕获或声明非检查异常),不同的是编译器对它们的区别对待(检查异常必须要在代码里处理,非检查异常就不需要),以及程序员对它们的区别对待(这需要程序员遵循良好的实践原则)。
这三类异常全部覆盖了第一节中所描述的异常发生场景:
④⑤⑥处可能会发生Checked Exception,
②③处既可能会发生RuntimeException也可能会发生Checked Exception,
⑦⑧⑨处可能会发生Error。
①处已经超出了Java异常处理机制的范畴(这属于容器要考虑的问题),通常在数据中加入返回码来通知异常信息。
异常的限制
先来看看 编程思想异常处理中的一段代码
class BaseballException extends Exception {
}
class Foul extends BaseballException {
}
class Strike extends BaseballException {
}
abstract class Inning {
public Inning() throws BaseballException {
}
public void event() throws BaseballException {
// Doesn’t actually have to throw anything
}
public abstract void atBat() throws Strike, Foul;
public void walk() {
} // Throws no checked exceptions
}
class StormException extends Exception {
}
class RainedOut extends StormException {
}
class PopFoul extends Foul {
}
interface Storm {
public void event() throws RainedOut;
public void rainHard() throws RainedOut;
}
public class StormyInning extends Inning implements Storm {
// OK to add new exceptions for constructors, but you
// must deal with the base constructor exceptions:
public StormyInning()
throws RainedOut, BaseballException {
}
public StormyInning(String s)
throws Foul, BaseballException {
}
// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
// Interface CANNOT add exceptions to existing
// methods from the base class:
//! public void event() throws RainedOut {}
// If the method doesn’t already exist in the
// base class, the exception is OK:
public void rainHard() throws RainedOut {
}
// You can choose to not throw any exceptions,
// even if the base version does:
public void event() {
}
// Overridden methods can throw inherited exceptions:
public void atBat() throws PopFoul {
}
public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch (PopFoul e) {
System.out.println("Pop foul");
} catch (RainedOut e) {
System.out.println("Rained out");
} catch (BaseballException e) {
System.out.println("Generic baseball exception");
}
// Strike not thrown in derived version.
try {
// What happens if you upcast?
Inning i = new StormyInning();
i.atBat();
// You must catch the exceptions from the
// base-class version of the method:
} catch (Strike e) {
System.out.println("Strike");
} catch (Foul e) {
System.out.println("Foul");
} catch (RainedOut e) {
System.out.println("Rained out");
} catch (BaseballException e) {
System.out.println("Generic baseball exception");
}
}
}
允许方法申明抛出某种异常,但在方法中不实际抛出,如 在Inning 中的构建器和event()方法。同样的道理也适用于abstract 方法,如atBat()里展示的那样。
在覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。异常说明本身并不属于方法类型的范畴中,因此不参与重载的判断。基于特定方法的“异常说明的接口”不是变大了而是变小了,小于等于基类异常说明表
比如StormyInning中的atBar()方法只实现了Inning中Foul异常的子类PopFoul异常,甚至event()方法没有实现任何的异常。
void walk() throws PopFoul {} //错误的实现
walk方法时不能通过编译的,因为PopFoul方法并未在基类中实现。
异常限制对构造器不管用,子类中只需包含基类构造器的异常说明。并且子类构造器中不可捕获基类构造器抛出的异常。
比如在StormyInning 中,构建器除了抛出基类的BaseballException外,一个还抛出了RainedOut,另一个抛出了Foul异常。然而,由于必须坚持按某种方式调用基础类构建器(在这里,会自动调用默认构建器),所以子类构建器必须在自己的异常规范中声明所有基础类构建器异常。
当处理派生类对象时,编译器只会强制要求捕获派生类该方法产生的异常。如果向上转型为基类,编译器会要求捕获基类方法产生的异常。如main函数所示。
异常使用指南
应该在下列情况下使用异常:
1) 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
2) 解决问题并且重新调用产生异常的方法。
3) 进行少许修补,然后绕过异常发生的地方继续执行
4) 用别的数据进行计算,以代替方法预计会返回的值。
5) 把当前运行环境下能做的事情尽量昨晚,然后把相同的异常重抛到更高层。
6) 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
7) 终止程序
8) 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人)
9) 让类库和程序更安全。(这既是在为调试做短期投资,也是为程序的健壮性做长期投资)
参考文档:
java编程思想-第12章 通过异常处理错误
https://www.cnblogs.com/hihtml5/p/6505801.html
https://www.cnblogs.com/hihtml5/p/6505994.html