异常分类
Java将异常分为两种,Checked异常和Runtime异常。Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无须处理。
try-catch块
如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出异常,同时try块后续代码通常将得不到执行。当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获异常:如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。
注意不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此退出。
当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常,否则再次拿该异常对象和下一个catch块里的异常类进行比较。try块后可以有多个catch块,这是为了针对不同的异常类提供不同的异常处理方式。当系统发生不同的意外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。
注意:try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块中不能访问该变量。
异常类的继承体系
Java提供了丰富的异常类,这些异常类之间有严格的继承关系。
Java把所有的非正常情况分成两种:异常和错误,它们都继承Throwable父类。Error错误一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error异常及其任何子类。
进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面,否则将出现编译错误。
Java7提供的多异常捕获
在Java7以前,每个 catch块只能捕获一种类型的异常,但从Java7开始,一个catch块可以捕获多种类型的异常。使用一个catch块捕获多种类型的异常时需要注意如下两个地方:1.捕获多种类型的异常时,多种异常类型之间用竖线|隔开。2.捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
使用finally回收资源
有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行(程序将要运行到return语句之前,会跳转到finally块中执行,执行完毕后会再执行之前的return语句)。在通常情况下,一旦在方法里执行到return语句的地方,程序将立即结束该方法,现在不会了,虽然return语句也强制方法结束,但一定会先执行finally块里的代码,然后再执行前面的return语句。但是如果在异常处理的catch块或者try块中执行System.exit(1)语句来退出虚拟机,finally块将失去执行的机会。
异常处理语法结构中只有try块是必需的,catch块和finally块都是可选的,但catch块和finally块至少出现其中之一。
当Java程序执行try块或catch块时遇到了return或throw语句时,这两个语句本都会导致该方法立即结束,但是系统并不会立即执行这两个语句,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来执行try块或catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块或catch块里的任何代码。
Java7的自动关闭资源的try语句
当程序使用finally块关闭资源时,程序显得异常臃肿。
Java7增强了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号中可以声明和初始化一个或多个资源。此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接或网络连接等),try语句在自身结束时自动关闭这些资源。
需要指出的是,为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法。Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。Closeable接口里的close()方法声明拋出了IOException,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类;AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明拋出任何异常。
自动关闭资源的try语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块也没有finally块。如果程序需要,自动关闭资源的try语句后也可以带多个catch块和一个finally块。
~Checked异常和Runtime异常
Java的异常被分为两大类:Checked异常和Runtime异常。
Java认为Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,将无法通过编译。对于Checked异常的处理方式有如下两种:1.当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常,然后在catch块中处理该异常。2.当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出异常。即对于Checked异常,要么显式声明拋出,要么显式捕获并处理它。
Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try...catch块来实现。
使用throws声明抛出异常
使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
throws声明抛出的语法格式如下:throws ExceptionClass1,ExceptionClass2...
一旦使用throws语句声明抛出该异常,程序就无须使用try...catch块来捕获该异常了。如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说调用该方法时要么放在try块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中。
使用throws声明抛出异常时有一个限制,就是方法重写时的一条规则:子类方法(重写了父类的方法)声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法(重写了父类的方法)声明抛出的异常不允许比父类方法声明抛出的异常多。
使用Checked异常至少存在如下两大不便之处:1.对于程序中的Checked异常,Java要求必须显式捕获并处理该异常或者显式声明抛出该异常,这样就增加了编程复杂度。2.如果在方法中显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。
在大部分时候推荐使用Runtime异常,而不使用Checked异常。尤其当程序需要自行抛出异常时,使用Runtime异常将更加简洁。当使用Runtime异常时,程序无须在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。如果程序需要在合适的地方捕获异常并对异常进行处理,则一样可以使用try…catch块来捕获Runtime异常。
使用throw抛出异常
当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成。 throw语句抛出的是一个异常实例,而且每次只能抛出一个异常实例。当Java运行时接收到开发者自行抛出的异常时,同样会中止当前的执行流,跳到该异常对应的catch块,由该catch块来处理该异常。也就是说,不管是系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有任何差别。
如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throws声明抛出的方法中;程序既可以显式使用try...catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。抛出Checked异常则可以让编译器提醒程序员必须处理该异常。
自定义异常类
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承Runtime Exception基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器,另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。
catch和throw同时使用
前面介绍的异常处理方式有如下两种:1.在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常。2.该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理。
在实际应用中往往需要更复杂的处理方式,当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。
Java7增强的throw语句
异常链
对于一个真实的企业级应用来说,当业务逻辑层访问数据持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面。通常的做法是:程序先捕获原始异常,然后在catch块中再throw一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。
这种捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理(23种设计模式之一:职责链模式),也被称为"异常链"。
在JDK4以前,程序员必须自己编写代码来保持原始异常信息。从JDK4以后,所有Throwable的子类在构造器中都可以接收一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。例如希望通过SalException去追踪到最原始的异常信息,则可以将该方法改写为如下形式。
上面程序代码创建SalException对象时,传入了一个SQLException和Exception对象,而不是传入了一个String对象,这就需要SalException类有相应的构造器。从JDK4以后,Throwable基类已有了一个可以接收Exception参数的方法,所以可以采用如下代码来定义SalException类。
Java异常跟踪栈
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪异常触发的过程。
面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成"方法调用栈",异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者…...直至最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。
一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传直到最外部业务操作的起点,通常就是程序的入口main方法或Thread类的run()方法(多线程的情形)。
异常传播到Thread类的run方法就会结束(如果该异常没有得到处理,将会导致该线程中止运行)。