Java异常体系
体系结构
Java异常类体系
说明
Throwable是所有异常的基类,它有两个子类:Error和Exception。
Error表示系统错误或资源耗尽,由Java系统自己使用,应用程序不应抛出和处理(如虚拟机错误:VirtualMachineError、内存溢出错误:OutOfMemoryError、栈溢出错误:StackOverflowError)。
Exception表示应用程序错误,它有很多子类,应用程序也可以通过继承Exception或其子类创建自定义异常,其三个直接子类:IOException(输入输出I/O异常)、RuntimeException(运行时异常)、SQLException(数据库SQL异常)。在Exception中RuntimeException比较特殊,属于非受检异常(unchecked exception),而其他Exception自身和其他子类则是受检异常(checked exception),另外,Error及其子类也是非受检异常。
checked和unchecked的区别在于Java如何处理这两种异常。对于checked异常,Java会强制程序员进行处理,否则会编译错误,而对于unchecked异常则没有这个要求。
常见RuntimeException
异常 | 说明 | 异常 | 说明 |
---|---|---|---|
NullPointerException | 空指针异常 | NumberFomatexception | 数字格式错误 |
IllegalStateException | 非法状态 | IndexOutOfBoundsExcpetion | 索引下标越界 |
ClassCastException | 非法强制类型转换 | ArrayIndexOutOfBoundsException | 数组索引越界 |
IllEgalArgumentException | 参数错误 | StringIndexOutOfBoundsException | 字符串索引越界 |
Throwable类有两个主要参数:一个是message,表示异常消息;另一个是cause,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层触发,cause表示底层异常。
Throwable说明
构造方法:
public Throwable() public Throwable(String message) public Throwable(String message, Throwable cause) public Throwable(Throwable cause)
Throwable类有两个主要参数:一个是message,表示异常消息;另一个是cause,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发,cause表示底层异常。Throwable还有一个public方法用于设置cause:
Throwable initCause(Throwable cause)
Throwable的某些子类没有带cause参数的构造方法,就可以通过这个方法来设置,这个方法最多只能被调用一次。在所有构造方法的内部,都有一句重要的函数调用:
fillInStackTrace();
它会将一次栈信息保存下来,这是我们能看到异常栈的关键。
异常处理
处理的方式包括:catch、throw、finally、try-with-resources和throws
catch匹配
catch可以多条:
try { // 可能异常代码 } catch (NumberFormatException e) { System.out.println("not valid number " + e.getMessage()); } catch (RuntimeException e) { System.out.println("runtime exception " + e.getMessage()); } catch (Exception e) { e.printStackTrace(); }
异常处理机制将根据抛出的异常类型找到第一个匹配的catch块,找到后,执行catch块内的代码,不再执行其他catch块,如果没有找到,会继续到上层方法中查找。需要注意的是,抛出的异常类型是catch中声明异常的子类放在前面,如果积累Exception放在前面,则其他更具体的catch代码将得不到执行。
其他:多个异常可用'|'拼接
try { // 可能异常代码 } catch (NumberFormatException | IndexOutOfBoundsException e) { System.out.println("not valid number " + e.getMessage()); }
异常链
对于NumberFormatException重新抛出了一个AppException,并且把当前的Exception e作为cause传递给了AppException,这样就形成了一个异常链,而在main方法中捕获了AppException就能获取到上一层的cause信息。
为什么要重新抛出呢?
因为当前代码不能完全处理该异常,需要调用者进一步处理。
finally
catch后面还可以跟finally语句
try{ // 可能异常 } catch(Exception e){ // 捕获异常 } finally{ // 不管有无异常都执行 }
finally内的代码不管有无异常发生,都会执行,具体来说:
1. 如果没有异常发生,在try内的代码执行结束后执行 2. 如果有异常发生且被catch捕获,在catch内的代码执行结束后执行。 3. 如果有异常发生但没被捕获,则在异常被抛给上层之前执行
由于finally这个特性,它一般用于释放资源,如数据库连接、文件流等。
try/catch/finally语法中,catch不是必须的,也可以是try/finally:表示不捕获异常,异常自动向上传递,但finally中的代码在异常发生后也执行。
finally语句有一个执行细节:如果在try或者catch语句内有return语句,则return语句在finally语句执行结束后才执行,但finally并不能改变返回值:
public static int test1(){ int res = 0; try { return res; } finally { res = 2; } }
这个函数test1返回值为0,而不是2。实际执行过程:在执行到try内的return res; 语句前,会先将返回值res保存在一个临时变量中,然后才执行finally语句,最后try再返回那个临时变量,finally中对res的修改不会被返回。
如果在finally中也有return语句呢?try和catch内的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖try和catch内的返回值,还会掩盖try和catch内的异常,就像异常没有发生一样,比如:
public static int test2(){ int res = 0; try { int a = 5 / res; return res + 1; } finally { return res + 2; } }
这个函数test2的返回值是2,并且不会抛出异常,即原本要抛出ArithmeticException就会被覆盖掉。
public static int test3() throws AppException { try { int a = 2 / 0; }finally { throw new AppException("finally 处理异常"); } }
这个函数test3将会抛出AppException异常,异常信息为"finally 处理异常",覆盖了try中将要抛出的ArithmeticException异常
所以,一般而言,为了避免混淆,应该避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。
如何使用异常
异常应该且仅用于异常情况
是指异常不能代替正常的条件判断,比如一个循环不能到到下标越界后再结束异常,应该先检查下标是否有效再进行处理。再如空指针异常,应该先判断是否为null再做处理。
另一方面,真正出现异常的时候,应该抛出异常来暴露问题,而不是返回特殊值。
异常处理的目标
异常大概可以分为三种来源:用户、程序员、第三方。用户是指用户的输入有问题;程序员是指编程错误;第三方泛指其他情况,如I/O错误、网络、数据库、第三方服务等。每种异常都应该进行适当的处理。
处理的目标可以分为恢复和报告。
恢复是指通过程序自动解决问题。报告的最终对象可能是用户,即程序使用者,也可能是系统运维人员或者程序员。报告的目的也是为了恢复,但这个恢复经常需要人的参与。
对用户:如果用户输入不对,可以提示用户具体哪里输入不对,如果是编程错误,可以提示用户系统错误、建议联系客服,如果是第三方连接问题,可以提示用户稍后重试。
对系统运维人员或程序员,他们一般不关心用户输入错误,而关注编程错误或第三方错误,对于这些错误,需要报告尽量完整的细节,包括异常链、异常栈等,以便尽快定位和解决问题。
异常处理的一般逻辑
如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。
如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为cause重新抛出一个异常。
总有一层代码需要为异常负责,可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是主程序。如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。
这个逻辑与在公司中处理问题的逻辑是类似的,每个级别都有自己应该解决的问题,自己能处理的自己的处理,不能处理的就应该报告上级,把下级的告诉他的和他自己知道的一并告诉上级,最终,公司老板必须要为所有问题负责。每个级别既不应该掩盖问题,也不应该逃避责任。