1.什么是异常
异常是大部分编程语言对程序运行“异常情况“”的一种处理机制,因为这种机制大大降低了编写和维护可靠程序的门槛。如今,异常处理机制已经成为现代编程语言的标配。
2.异常的分类
在java
编程语言中,异常可以分为两种,运行时异常和编译时异常。对于运行时异常,在编写代码的时候,可以不用主动去捕获。而对于编译时异常,编译器会在对代码进行编译时,检查被抛出的异常是否被处理(处理方式主要两种:捕获和在函数定义中声明),否则会产生编译错误。
(1).受检异常
编译时异常因为会受到编译器的检查,保证异常被处理,因此该异常也被称为受检异常。像IOException
,受检异常通常有以下特点:
1.受检异常通常需要显示的在函数定义中声明,如果一个函数会抛出很多受检异常,那么函数定义就会很冗长。
2.因为受检异常需要在代码层面被处理,如果给一个函数增加了一个受检异常,那么这个函数所在的函数调用链上的所有位于其之上的函数都需要做相应的代码修改,直到调用链中的某个函数将这个新增的异常进行捕获。由此可见,受检异常的这种处理机制,会违反开闭原则。
虽然受检异常存在以上不足,让代码中充斥着很多异常处理逻辑,代码结构看起来比较"繁琐",不够整洁,但是带来的好处就是,编译器帮我们"未雨绸缪"地检查出可能出现异常的逻辑,让我们提前对可能现异常的逻辑,进行处理,使程序的运行完全在我们的掌控之中,使程序处于一种可控状态。
(2).非受检异常
相比于非受检异常,受检异常的处理,就变得更加的灵活,怎么处理的主动权完全交给了程序员,但是过度的灵活带来的问题就是不可控,因为非受检异常不需要显示地在函数定义中声明,也不需要强制进行捕获处理,很多情况下,都是当异常情况发生了,对业务产生影响了,我们才会发现:“原来这段代码在某些条件下会产生异常”。所以受检异常和非受检异常,各自有各自的不足和优势,在使用时,结合所处的场景进行合理的选择。
3.异常该如何处理
无论受检异常还是非受检异常,我们都可以对其进行捕获并处理,只不过受检异常是编译器强制我们进行处理,非受检异常由我们自己决定。
那么当我们捕获到了异常之后,如何对异常进行处理呢?这个其实是一个很难回答的问题,因为怎么处理要结合异常发生的场景,这个看似是一个"填空题",但是结合日常异常处理的经验,可以将其转换成一个"选择题"。
其实对于异常的处理,我们可以类比工作中,我们对突发情况的处理。假如你是一个团队的负责人,在负责团队工作中,出现了一些突发情况,你会怎么处理呢?
这个时候,作为负责人的你可能会分析该问题产生的原因和可能解决方案,以及解决方案需要的资源,来决定是否需要向上级回报,还是可以直接在团队内部解决。
如果是开发过程一个小bug,且对项目没有太大影响的话,完全可以在团队内部解决。
但是如果是项目出现了重大事故,对业务造成很大影响的话,这个可能就不是团队负责人自己可以承担的了,需要向上级领导汇报,由上层领导来决定如何处理。
回到程序中对异常的处理,就是当出现异常时,我们是否需要将异常往上抛的问题,此时抛出异常的函数就是团队内部,上级领导就是该函数的调用方。
再回到我们的例子中,当团队负责人无法决定如何处理突发情况,需要向上级领导汇报时,该如何汇报呢?这个需要结合突发情况的性质,以及上级领导是否对该团队以及团队所做的业务有充分的了解。
如果上级领导对团队构成以及所做的业务十分了解的话,那么团队负责人直接把突发情况原封不到的转述给上级领导即可,让上级直接基于最原始的信息对突发问题进行处理。
如果上级领导对团队业务不甚了解,那么团队负责人在汇报问题的时候,就不能过多的描述突发问题细节甚至一些技术性的解决方案,因为这些细节领导可能听不懂,也不关心,那么此时团队负责人应该对问题进行简要的总结,抓住问题的重点进行汇报。
回到程序中对异常的处理,就是在决定需要将异常往上抛的时候,结合调用方是否了解函数内部实现细节,以及调用方是否需要这些细节信息,来帮助其进行异常的处理。
如果调用方了解的话,那么就可以将异常原封不动的抛上去,如果调用方对函数内部实现细节不了解,而且调用方仅仅只需要知道函数调用发生了异常,不关心为什么发生了异常,原因是什么等,那么就不能再将原始异常直接抛给调用方了,因为原始的异常,调用方不关心也不认识,捕获到异常后,压根就不知道怎么处理。
所以结合上面对于异常处理的分析,我们把异常处理这个"填空题" 转换成 “选择题”,而且这个选择题的选项大致有以下三个:
1.打印一些关键日志信息,然后直接吞掉。
2.打印一些关键之信息,然后原封不动的将异常往上抛。
3.打印一些关键信息,然后基于异常信息,包装成更加符合业务的自定义异常。
为了帮助理解,下面我举一个简单的例子说明一下,从存储系统中,根据key获取value。
public String getProperty(String key) {
try {
return getValueFromFile(key);
} catch (IOException e) {
log.error("read file failure ", e);
throw new BasicException("", "read file failure");
}
}
private String getValueFromFile(String key) throws IOException {
Properties prop = new Properties();
InputStream in = Object.class.getResourceAsStream("/config.properties");
prop.load(in);
String value = prop.getProperty(key).trim();
return value;
}
getValueFromFile
表示从配置文件中获取指定key
对应的value
值。在实现过程中,会因为文件加载和解析等问题导致value
获取失败,此时getValueFromFile
应该将获取失败的异常抛给调用方getProperty
,因为我们并不清楚调用方如何使用这个value
值,如果贸然的返回一个默认值,可能会让调用方误解为该默认值是正常值,对业务产生影响。
那么如果把异常抛给调用者,是把异常原封不同的抛出去还是进行一定的处理后,在抛出去呢?
在这里应该原封不动的抛出去。首先,getValueFromFile
方法的签名,就表明了实现细节会涉及文件操作,会出现涉及文件操作的异常,而且对于调用方getProperty
来说,它既然调用了这个方法,就应该知道value
值是从文件中获取的,有责任和义务来处理关于文件相关的异常。
那么当 getProperties
捕获到 getValueFromFile
抛出的IOException
后,应该怎么处理呢?
应该将异常封装后在进行抛出,这里没有直接将IOException
抛出,是因为,getProperty
方法签名仅仅表示获取属性值,没有表示任何和实现细节相关的说明。对于getProperty
的调用方来说,只是想根据key
获取value
,并不了解这个value
从哪里来的(具体储存在什么存储系统中),如果getProperty
将IOException
原封不动的抛出,那么对于其调用方来说,是不知道该怎么处理的,而且也向调用方暴露了过多的实现细节,增加了调用方业务处理的复杂度。
4.可以用异常来实现逻辑控制吗
异常可以作为函数调用方在调用函数的过程中,当出现异常情况时,两者之间异常信息传递的媒介。让函数调用方捕获到函数中发生的异常,然后结合异常来进行业务的处理。
但是这种用异常来控制业务逻辑的做法,却不是java
中异常的最佳实践,原因java
中对于异常的创建是一个比较重的操作。
java
中创建异常时,会将当前线程的栈信息记录下来,进行保存,这也是我们可以使用异常对象,将堆栈打印出来的原因。
查看异常构造方法可以发现,异常构建的时候会调用fillInStackTrace
这个方法会将当前线程中所有栈帧信息都记录起来,这个操作比较耗费性能的,所以很多关于异常的介绍都不建议使用异常来进行业务逻辑的控制手段。
/**
* Fills in the execution stack trace. This method records within this
* {@code Throwable} object information about the current state of
* the stack frames for the current thread.
*
* <p>If the stack trace of this {@code Throwable} {@linkplain
* Throwable#Throwable(String, Throwable, boolean, boolean) is not
* writable}, calling this method has no effect.
*
* @return a reference to this {@code Throwable} instance.
* @see java.lang.Throwable#printStackTrace()
*/
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace(0);
stackTrace = UNASSIGNED_STACK;
}
return this;
}
private native Throwable fillInStackTrace(int dummy);