吃透Java异常体系

Java的异常都是派生于Throwable类的一个实例,所有的异常都是由Throwable继承而来的。Throwable有分为了Error类和Exception类。

Error(错误)

Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。Error表示比较严重的问题,一般是JVM运行时出现了错误,如没有内存可分配抛出OOM错误、栈资源耗尽抛出StackOverflowError错误、Java虚拟机运行错误Virtual MachineError、类定义错误NoClassDefFoundError。如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地终止之外, 再也无能为力了。

Exception(异常)

异常又分为RuntimeException和其他异常。由程序错误导致的异常属于RuntimeException,而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。

运行时异常RuntimeException:顾名思义,运行时才可能抛出的异常,编译器不会处理此类异常。比如数组索引越界、使用的对象为空、强制类型转换错误、除0等等。出现了运行时异常,一般是程序的逻辑有问题,是程序自身的问题而非外部因素。

其他异常:Exception中除了运行时异常之外的,都属于其他异常。也可以称之为编译时异常,这部分异常编译器要求必须处置。这部分异常常常是因为外部运行环境导致,因为程序可能运行在各种环境中,如打开一个不存在的文件,此时抛出FileNotFoundException。编译器要求Java程序必须捕获或声明所有的编译时异常,强制要求程序为可能出现的异常做准备工作。

不要被运行时异常的名称所迷惑,理论上所有的错误都是运行时发生的。包括Error、RuntimeException、编译时异常等等。所有的这些都只能在程序运行的过程中才能碰到。编译时异常指的是编译器要求必须处理的异常,并不是代码编译间发生的错误。

受检异常和非受检异常

字面理解,接受检查的异常和不接受检查的异常。根据上面的信息,Error和RuntimeException运行时异常都不能被检查。他们都是程序运行过程中所产生的,只是Error的错误比较严重,一般是JVM产生,而RuntimeException一般是程序逻辑自身的问题。受检异常就是上面的编译时异常,这些异常在编译时被强制要求捕获或者声明。编译器将会为所有的受检异常提供异常处理器。

PS:其实异常发生了,除了更改程序或者配置等,没有其他的方法。只是对引起程序不正常工作的原因进行分类,就出现了Error和Exception(运行时异常和编译时异常)。这种分类能让程序员更好的定位因此错误的原因,更方便更高效的进行开发,写出健壮性更好的代码。但是异常并不能改变当前运行的结果,因为从程序开始运行的那一刻,整个逻辑和数据都已经固定了。

异常处理机制

主要是try-catch-finally和throw、throws关键字。

异常都是派生于Throwable类的一个实例,这个实例可以由JVM产生,也可以在程序中手动创建,用throw手动抛出。throw的对象必须是派生于Throwable类的实例,其他类型无法通过编译。

受检异常只有两种选择。要么被捕获处理,要么被抛出,让调用者处理。而非受检异常没有此强制要求。

throws是用来声明异常,只要是派生于Throwable类的都可以被声明。方法应该在其首部声明所有可能抛出的受检异常,这是强制要求的。非受检异常不要求必须通过throws声明,因为Error发生后对其无能为力,而如果有运行时异常,那么就是程序自身的问题,应该把时间花在修正程序的错误上,而不是说明程序发生的可能性上。所以编写程序的时候throws关注点是受检异常,但是非受检异常也可以通过throws声明,只是不强制要求而已。若有多个受检异常,必须在throws中全部声明,如果方法没有声明所有可能的受检异常,编译器就会发出错误提示。

try-catch-finally用来捕获异常。所有派生于Throwable类都可以通过catch捕获,try中放可能存在异常的方法,如果在 try语句块中的任何代码抛出了一个在 catch 子句中说明的异常类,那么程序将跳过 try语句块的其余代码,并且执行 catch 子句中的处理器代码。如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch 子句,如果方法中的任何代码拋出了一个在 catch 子句中没有声明的异常类型,那么这个方法就会立刻退出。

在一个 try 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以为每个异常类型使用一个单独的 catch 子句。需要注意,如果多个catch中的异常非继承关系,那么catch顺序不影响结果,如果catch异常存在类继承关系,那么子类的catch应该放在前面,父类的在后面。如下图,catch的判断是从上到下,如果父类在前,那么派生于父类的都会被捕获,执行父类的处理代码,这样导致后面子类的处理代码永远不会被执行。

finally一般用来关闭所占用的资源。如果代码抛出异常,就会终止剩余代码的处理,并且退出这个方法。这样可能会导致一些程序占用的系统并不能被正确的释放。而不管是否有异常被捕获,finally中子句的代码都会被执行,可以在这里正确的释放资源。

抓抛模型

所有的异常,一定是先有一个实例然后抛出。这个可以JVM完成,也可以手动执行。这个实例是整个异常处理的源头。然后调用类可以利用try-catch-finally对异常进行捕获,也可以在方法首部通过throws继续向上层抛出。try-catch-finally是抓,throw/throws是抛。一个类遇到异常,要么捕获后处理,要么继续向上抛出,让调用者进行处理。

catch中可以为空,这样程序就会忽略掉哪些异常。不进行处理本身就是一种处理方式。在catch中也可能抛出异常,也可以手动抛出。这样可以改变异常的类型,使用异常的包装技术,原先的异常时新异常产生的原因,可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

try-catch-finally可以灵活组合。可以try-catch、try-finally或者try-catch-finally。可以分为以下几种情况

1.try中执行正常,忽略catch,最后执行finally。

2.try中出现异常,且异常在catch中声明,执行catch,最后执行finally.

3.try中出现异常,但catch中未声明,不执行catch,最后执行finally.

4.try中出现异常,且异常在catch中声明,执行catch时出现异常,停止catch中代码,执行finally并且抛出catch中新出现的异常。

finally

finally不管是try或者catch中没有由异常,最后大概率都会执行。这是因为编译器会讲 finally 块中的代码复制两份并分别添加在 try 和 catch 的后面。但是如果try或者catch中出现了 System.exit()语句,则会直接退出,并不会执行finally模块。

finally中没有return语句 最后返回值为2

finally中有return语句 最后返回值为3

上面两个代码只是在finally中是否存在return语句的区别,但直接结果却并不相同。可以看到如果finally中没有return语句,程序就会把finally中的操纵数据忽略掉。其实finally中的数据操作也是执行了的,但是并没有返回。这是因为在return语句返回之前,虚拟机会将待返回的值压入操作数栈,等待返回。即使 finally 语句块对 i 进行了修改,但是待返回的值已经确实的存在于操作数栈中了,所以不会影响程序返回结果。

在try中的数据处理完,检测到有return语句时,会先将数据压入操作数栈等待返回,然后去执行finally语句,如果finally语句没有将新的结果压入操作数栈,那么只可能返回原先的结果。从这个角度理解,即使finally中对数据处理,但是返回的依旧时try中的“脏数据”。finally并不是没有执行,而是执行了却没有没返回。添加return语句则会将finally处理过的数据压入操作数栈返回,原先的“脏数据”失效。

注意这里指的是基本变量,如果是引用类型不受此影响。因为不管在哪里进行运算,处理的都是引用背后的“实体”。

Throwable

Throwable是顶层的异常类,下面是 Throwable 类的主要方法:

1.public String getMessage()

返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了

2.public Throwable getCause()

返回一个Throwable 对象代表异常原因

3.public String toString()

使用getMessage()的结果返回类的串级名字

4.public void printStackTrace()

打印toString()结果和栈层次到System.err,即错误输出流

5.public StackTraceElement [] getStackTrace()

返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底

6.public Throwable fillInStackTrace()

用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中

自定义异常

Java 的异常机制中所定义的所有异常不可能预见所有可能出现的错误,某些特定的情境下,则需要我们自定义异常类型来向上报告某些错误信息。

一般地,用户自定义异常类都是RuntimeException的子类

自定义异常类通常需要编写几个重载的构造器

自定义异常需要提供 serialVersionUID 序列化唯一ID,方便调试

自定义异常最重要的是异常类的名字,当异常出现时,可以根据 名字判断异常类型

注意事项

当子类重写父类带有throws声明的函数时,其声明的异常范围必须要在父类的支持范围内,即范围不能比父类大。只能保持范围不变或者更精确,不能变大。如果父类没有throws,那么子类也不能有throws,受检异常必须在子类内部捕获处理。

Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。

有兴趣了解更多编程基础知识可以观看教学视频继续学习。异常处理不能代替简单的测试。捕获异常所花费的时间要远远的超过了测试的时间,所以在可能的情况下,先对执行条件进行判断,而不要等执行出错之后捕获异常。

猜你喜欢

转载自juejin.im/post/7039892311030890510
今日推荐