在正常状况下使用异常处理, 无疑会降低系统的效率,以及编码的效率
解决思路:
正常的状况和异常的状况一定要分开、 分清, 不能混用
没有分清异常的类别
- 非正常异常(Error) : 我们通常使用“错
误”这个词汇而不是“异常”来表示
这类异常的命名以Error结尾, 比如
OutOfMemoryError, NoSuchMethodError。 这类异常, 编译器编译时不检查, 应用程序不需
要处理, 接口不需要声明, 接口规范也不需要纪录;
- 运行时异常(RuntimeException) :又叫非检查型异常(UncheckedException)
这类异常的命名通常以Exception结尾, 比如
IllegalArgumentException, NullPointerException。
这类异常, 编译器编译时不检查, 接口不需要声明, 但是应用程序可能需要处理, 因此接口规范需要记录清楚;
- 非运行时异常:又叫 检查型异常(CheckedException)
除了运行时异常之外的其他的正常异常都是非运行时异常, 比如
InterruptedException, GeneralSecurityException。 和运行时异常一样, 命名通常以Exception结尾。 这类异常, 编译器编译时会检查异常是否已经处理或者可以抛出, 接口需要声明, 应用程序需要处理, 接口规范需要记录清楚
我们可以用是否声明接口来区分 后两个异常.
三个异常详细区分图:
没有标记清楚抛出异常
应用程序需要处理异常(CheckedException和RuntimeException) , 就需要我们在方法的规范描述文档中清楚地标记异常。
没有标记的异常, 应用程序没有办法通过文档了解哪些异常需要处
理、 什么状况下会抛出异常以及该怎么处理这些异常。
对于检查型异常, 编译器或者IDE会友好地提醒使用合适的声明。 我们一般不会遗漏检查型异常的声明。 既然声明不会遗漏, 异常的标记也通常不容易遗漏。
然而, 对于运行时异常, 我们就没有这么幸运了。 目前我们使用的编译器或者IDE, 还没有提醒运行时异常遗漏的功能。 由于没有工具的帮助, 我们就很难发现运行时异常, 这就很容易导致代码效率降低, 错误增多。
我举个例子, 在上面的检查用户名的例子中, 如果我们不在方法的规范描述中记录抛出的运行时异常, 该方法的使用立即就会遇到问题。
其中最常见的问题包括:
- 如果参数userName是一个无效引用(null) , 会发生什么状况, 该怎么处理?
- 如果参数userName是一个空字符串(“”) , 会发生什么状况, 该怎么处理?
- 如果参数userName不是一个规范的用户名, 会发生什么状况, 该怎么处理?
每一个问题, 都会降低使用者的效率, 让使用者陷入难以摆脱的困扰。
如果代码的层次结构再多一层, 这个问题就会更加严重:
if(){
} else if(){
...
}
如果一个方法
既没有异常的声明,
又没有异常的规范描述,
调用者一般不会进行异常处理, 也不在规范描述中加入抛出异常的描述。 这样的层次结构, 只要稍微多个一两层, 运行时异常虽然
在代码和规范描述层面消失得无影无踪, 但它并没有真正消失, 依然会在运行时准时出现。
即使调用者拥有源代码, 可以阅读源代码, 也不容易意识到有运行时异常需要谨慎对待。 代码的阅读者也不会有足够的精力和动力去深挖所有的层次, 来确认有没有运行时异常。
解决思路:
- 对于所有的可能抛出运行时异常, 都要有清晰的描述, 一个也不要错过;
- 查看所有的调用方法的规范描述, 确认抛出的异常要么已经处理, 要么已经规范描述。
处理好捕获异常
要想处理好异常, 我们需要了解异常机制的基本原理。 我们一起回顾一下Java异常的四个要
素:
- 异常类名(IllegalArgumentException, FileNotFoundException)
- 异常描述(“Invalid file path”)
- 异常堆栈(at sun.security.ssl.InputRecord.read(InputRecord.java:504))
- 异常转换(Caused by: javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?)
这四个要素满足了三个现实的异常处理问题:
- 出了什么错?2. 什么地方出了错?3. 为什么会出错?
其中,
异常类名 ------------解决了“出了什么错”的问题;
异常描述------------解决了“为什么会出错”的问题;
异常堆栈------------解决了“什么地方出了错”的问题;
而异常转换------------记录了不同场景对这三个问题的不同理解和不同处理。
其中JVM自动帮我们处理异常堆栈, 我们需要特别关注的就只有三点了。
老手教你应用
1.对于异常类名, 我们要准确地选择异常类。
Exception类是一个包罗万象的超级异常类, 如果我们使用Exception作为声明和抛出的异常, 就不方便用户精准定位, 从而解读和判断“出了什么错”。
类似的超级异常类还有
RuntimeException、 IOException等。
除非是超级的接口, 否则我们应该尽量减少超级异常类的
使用, 而是选择那些意义明确、 覆盖面小的异常类, 比如FileNotFoundException。
2.对于异常描述, 我们要清晰地描述异常信息。
虽然Java异常允许使用没有具体异常信息的异常, 但是这种使用却容易丢失用户对于“为什么会出错”这个问题更精准的解读。 所以我不推荐使用没有描述信息的异常。
3.对于异常转换, 我们要恰当地转换异常场景。
随着应用场景的转换, 我们还需要转换异常的类型和描述。
比如, SQLException这种涉及具体
实现细节的异常类就不太适合直接抛给最终的用户应用。 用户关心的是商业的逻辑, 并不是实现的细节, 这就需要我们随着使用场景调整异常。 如果一股脑儿地把所有的异常抛到底, 业务逻辑就会很混乱, 用户体验也不好。
但是随着场景调整异常也不是没有代价的。 这是一个妥协的选择, 会带来一些负面的情况。
第一个情况, 就是需要编写转换的代码, 这当然没有异常一抛到底方便。
第二个情况, 就是信息的冗余。 如果转换场景有两三层, 异常打印出来的堆栈信息就会很长, 而最有用的信息其实只有最原始的异常。
第三个情况, 就是信息的丢失。 有些信息的丢失是有意的, 比如对敏感信息的过滤而丢掉的异常信息。
有些信息的丢失是无意的过失。 信息的丢失很难让我们排查出异常问题, 于是错误的源头被硬生生地隐匿了起来。 所以, 除非有明确的需求, 我们要尽量保留所有的异常信息以及转换场景