Java/Java异常体系
Java 从诞生之初就提供了完善的异常处理机制,大大降低了编写和维护可靠程序的门槛。Java 的异常处理机制主要回答了 3 个问题:
- What:异常类型回答了什么被抛出
- Where:异常堆栈跟踪回答了在哪里被抛出
- Why:异常信息回答了为什么被抛出
Error 以及 Exception
Error 和 Exception 的区别
- Error:程序无法处理的系统错误,编译器不做检查,这种情况应该终止程序
- Exception:程序可以处理的异常,捕获后可以恢复,应尽可能在程序内处理这种异常
Exception 的分类:
- RuntimeException:这类异常是不可预知的,程序应该通过各种判断来自行避免
- 可预知的异常,编译器强制必须校验的异常。如果程序不处理,则编译不通过 不要抛出异常的父类来粗化异常
从责任角度看:
- Error 属于 JVM 需要承担的责任
- RuntimeException 属于程序应该负担的责任
- Checked Exception 可检查异常是 Java 编译器应该承担的责任
常见 Error 以及 Exception
- NullPointException:空指针引用异常
- ClassCastException:类型强制转换异常
- IllegalArgumentException:传递非法参数异常
- IndexOutOfBoundsException:下标越界异常
- NumberFormatException:数字格式异常
Checked Exception
- ClassNotFoundException:找不到指定 Class 的异常
- IOException:IO 操作异常
Error
- NoClassDefFoundError:找不到 Class 定义的异常
- 类依赖的 Class 或者 jar 不存在
- 类文件存在,但同一个类被多个类加载器重复加载,存在于不同的域中
- 大小写问题:javac 编译是无视大小写的,很有可能编译出来的 Class 文件与想要的不一样
- StackOverflowError:深递归导致栈被耗尽而抛出的异常
- OutOfMemoryError:内存溢出异常
下面是一个例子:
1 | public class ExceptionAndError { |
上面的代码里,第三个方法体throwCheckedException()
内的异常属于 CheckedException(可检查异常),编译器会强制要求捕获该异常,否则编译不能通过。 可以使用try-catch
把异常捕获,在catch
里面增加相应的处理逻辑,具体的处理逻辑需要依据业务需要而定,一般的原则是理解这种Exception
的原因,并结合实际业务处理,如下所示。
1 | private void throwCheckedException() { |
在实际场景中,一个项目是由很多模块组成的。假设上面的代码是模块 A 的,如果 B 调用了 A 的方法产生了异常,那么异常不应该由 A 使用try-catch
捕获,因为A 并不了解 B 的业务,因此 A 难以根据 B 的业务来处理Exception
,因此最佳实践是 A 把异常继续往上抛出,由上层调用者来处理异常,如下所示。
1 | //最佳实践:把异常往上抛出(并且不要抛出异常的父类,这样会粗化异常,丢失异常细节信息),由调用者来处理异常 |
在引发异常时,会在堆上创建异常对象,对象包含异常类型和程序运行状态等异常信息。然后中断当前正在执行的流程,由运行时系统去寻找合适的异常处理器。在catch
代码块 return 之前,会先执行 finally 代码块的语句,所以 finally 里的 return 会先于 catch 代码块的 return。
捕获异常时不要捕获异常的父类,会给团队中其他同事掩盖具体的异常信息,并且可能捕获到不希望捕获的异常。 不要生吞异常,如果不在第一时间抛出异常,程序会以不可控的方式结束,并且难以定位出错的地方
Java 异常的处理原则
- 具体明确:抛出的异常应该能够通过异常类名和 message 准确说明异常的类型和产生异常的原因
- 提早抛出:应尽可能早地发现并抛出异常,便于精确定位问题
- 延迟捕获:异常的捕获和处理应该尽可能延迟,让掌握更多信息的作用域处理异常
下面来对比if-else
和try-catch
的性能
1 | public class ExceptionPerformance { |
上面代码对比了if-else
和try-catch
的耗时,一般来说if-else
的耗时总会比try-catch
的耗时短
Java 异常处理消耗性能的地方:
- try-catch块影响 JVM 的优化,影响了指令重排序带来的优化
- 异常对象实例需要保存堆栈快照等信息,开销较大 所以建议仅捕获可能出现异常的必要的代码段,不要用 try-catch 包住整个代码段,也不要用 try-catch 来控制代码流程,因为 try-catch 远没有 if-else 和 switch 的效率高
高效主流的异常处理框架
- 设计一个通用的继承自 RuntimeException 的异常 AppException 来统一处理
- 其余异常都统一转译为上述异常 AppException
- 在 catch 之后,抛出上述异常 AppException 的子类,并提供足以定位异常位置的业务友好的信息,而非之前一大堆无关的堆栈信息
- 最后由前端接收 AppException 做统一处理