Java异常机制分析

概念

异常是指程序在运行期间所发生的错误,如使用了空指针、栈溢出、非法参数等。在程序编写期间,编译器会自动检查代码是否符合规范,并尽可能地帮助程序员将其纠正。但即使是看似正确的代码,也可能会在运行期间抛出一个意想不到的异常。

Java为此提供了异常处理机制,即在程序运行期间,倘若抛出了异常,则可以以适当的方式进行捕获处理,使得程序能够正常的运作下去。

体系结构

在Java中所有的异常类都是从java.lang.Throwable类集成的子类。

根类Throwable下(仅)有两个重要的子类——ErrorException

  • Error代表运行期间JVM(Java虚拟机)出现的异常,这种异常一般来说是无法处理的。
  • Exception代表运行期间程序本身的逻辑出现的异常,这种异常一般是程序本身可以处理的。

其中,Exception可分为两类:运行时异常和检查异常。

  • 检查异常(CheckedException),是指程序在执行某段代码时,是可以提前知道这段代码是存在潜在异常的,而且要求程序必须以某种方式来处理。若不处理这种异常情况时,编译器是不会通过编译的。
  • 运行时异常(RuntimeException),也称为非检查异常,是指程序在运行期间可能会抛出异常,但不要求程序必须处理该异常。在编译期间,编译器也不会要求用户去处理它。

TRY-CATCH会不会性能消耗

当初的自己觉得如果在try-catch块中大量使用循环的话,想当然的认为会消耗大量的性能。但是通过阅读多篇文章后,得出以下结论:

  • 异常如果没发生,也就不会去查异常表,也就是说你写不写try-catch,也就是有没有这个异常表的问题,如果没有发生异常,写try-catch对性能是木有消耗的,所以不会让程序跑得更慢。
  • try-catch 的范围大小其实就是异常表中两个值(开始地址和结束地址)的差异而已,也是不会影响性能的。

具体文章如下所示:

Try-Catch真的会影响程序性能吗

Java上的try catch并不影响性能(转)

优化建议

优化建议这一部分是结合了Java异常处理和设计异常处理的 15 个处理原则两文的精华,小弟只能做个低调的搬运工。

  • 只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程

即使在上述的说到异常机制不会怎么消耗性能,但这并不代表能够在程序中随处使用try-catch。要在程序中谨慎地使用异常,倘若异常使用过多仍然会很大程度上影响程序的性能。如果在程序中能够用if语句来进行逻辑判断,自然能更清楚地表明出当某个字段处于某个阶段时要进行的逻辑,也可以减少异常的使用,从而避免不必要的异常捕获和处理。

  • 切忌使用空catch

倘若程序在捕获了异常之后什么都不做,相当于你直接隐藏了这个异常,这可能会导致后面的程序逻辑出现不可控的执行结果,这是一种相当不负责任的行为。倘若有这种情况发生,不如改变程序本身的代码逻辑,使其变得更加健壮,并用日志的方式记录其异常的状态,方便日后的更新和维护。

  • 检查异常与非检查异常的选择

当你决定要抛出一个自己新定义的异常,你就要决定以什么形式来处理这个异常。

当有些检查异常对开发人员来说是无法通过合理的手段处理的,例如SQLException,这样就会导致在代码中经常出现的一种情况:逻辑代码很少几行,但是要进行异常捕获和异常处理的代码却有很多行,这会导致逻辑代码阅读起来晦涩难懂,使得代码难以维护。

在检查异常与非检查异常的选择上面,如果存在该异常情况的出现很普遍,需要特别提醒调用者注意处理的话,就是用检查异常,否则就使用非检查异常。

  • 注意catch块的顺序

切忌将捕获父类异常的catch块放置于捕获子类异常catch块前,否则将永远无法到达程序理想的异常处理逻辑状态中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
FileInputStream inputStream = new FileInputStream("d:/a.txt");
int ch = inputStream.read();
System.out.println("aaa");
return "step1";
} catch (Exception e) {
System.out.println("io exception");  
return "step2";
} catch (FileNotFoundException e) {
// 永远到不了这一步,因为catch块是从上到下优先匹配到符合该异常类及其父类
// 由于Exception为FileNotFoundException的父类,所以catch块将在第一次匹配中结束
System.out.println("file not found");    
return "step3";
}
  • 避免多次在日志信息中记录同一异常

很多情况下异常都是层层向上抛出,如果在每次向上抛出异常的时候,都记录到日志中,则会导致冗余的异常重复记录在日志中,不仅大量浪费空间,而且很难查找到异常的根源。

妥当的做法是只在异常最开始发生的地方进行日志信息记录。

  • finally中释放资源

如果在程序中存在文件读取、网络操作以及数据库操作等,需要在finally块中释放资源。这样不仅使得程序占用的资源更少,也会避免由于资源未及时释放而导致的异常情况。

  • 不要在finally中使用return语句

倘若在正常try块中返回值,又或者是,在捕获异常后打算在catch块中返回值的话,切忌在finally块中再返回值,否则finally的返回值将直接取代catch块中的返回值。这不难想象,因为finally块在try-catch执行完后一定会执行的,所以finally中的操作将会正常执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
FileInputStream inputStream = new FileInputStream("d:/a.txt");
int ch = inputStream.read();
System.out.println("aaa");
return "step1";
} catch (Exception e) {
System.out.println("io exception");  
return "step2";
} finally {
System.out.println("finally end"); 
// 程序执行到这,会导致最终的返回值是"step3",而非"step1",也不会是"step1"
return "step3";
}
  • 当方法判断出错该返回时应该抛出异常,而不是返回一些错误值

因为错误值在程序逻辑中可能会出现难以理解的情况,并且错误值在描述异常的情况并不直观。在文件找不到的时候,应当抛出类似 FileNotFoundException 异常,而不是返回 -1 或者 -2 之类的错误值。


参考资料

Java异常处理和设计

异常处理的 15 个处理原则