异常(Exception):指程序运行过程中出现的非正常现象。
1、 Java异常的异常处理机制
早期的情况:
早期使用的程序设计语言是没有提供专门进行异常处理功能的,程序设计人员只能苦逼的使用条件语句对各种可能设想到的错误情况进行判断,来捕捉特定的异常,然后进行相应的处理。这样的处理方式,往往要整出大段大段的if…else语句。本来需要完成相应功能的代码块很小,但是加上这样针对异常处理的条件语句使得代码显得非常臃肿,这样一来代码的可读性和可维护性就下降了,而且有时候还会遗漏意想不到的异常情况。
Java的出现:
针对上面的情况Java提供了强大的异常处理机制,可以说为程序猿带来了福音,Java的异常处理机制可以很方便的在程序中监视可能发生异常的程序块,并将所有的异常处理代码集中放在程序的某处,使完成正常功能的程序代码与进行异常处理的程序代码分开。通过异常处理机制,减少了编程人员的工作量,增强了异常处理的灵活性,并使得程序的可读性和可维护性大大的提高了。
在Java的处理机制中,引入了一些用来描述和处理异常的类,每个异常类反映一类运行的错误,在类的定义中包含了该类异常的信息和对异常进行处理的方法。对程序运行的过程中发生某一个异常现象时,系统就产生了一个与之相应的异常类对象,并交由系统中的相应机制进行处理,以避免系统崩溃或其他对系统有害的结果发生,保证了程序运行的安全性。
2、 Java异常类的定义
Java中,把异常分为:错误(Error)和异常情况(Exception)。
错误(Error):指程序本身存在非法的情形,这些情形常常是因为代码存在着问题而引起的。而且编程人员可以通过对程序进行更新仔细的检查,把这种错误的情形减小到最小,从理论上讲,这些情形是可以避免的。
异常情况(Exception):表示另外一种“非同寻常”的错误。这种错误通常是不可预测的。常见的异常情况包括内存不足,找不到所需要的文件等。
Throwable类派生了两个子类:Exception和Error。
Error类描述内部的错误,它由系统保留,程序不能抛出这个类型的对象,Error类的对象不可捕获,不可以恢复,出错时系统通知用户并终止程序;
Exception类则供应程序的使用,所有的Java异常类都是系统类库中的Exception类的子类。
继承关系:
Throwable类是所有异常类的父类,实现了Serializable接口。Throwable有两个很重要的子类:Exception和Error,分别表示异常和错误。异常一般表示可检测、可恢复或者在编码中可以避免的问题,一般是比较轻度的错误;错误通常表示严重的问题,大多数情况下与代码的执行无关,比如OutOfMemoryError。
其中受检的异常表示期望调用者在异常发生时能够采取一些恢复的措施,或者将该异常传播出去。比如我们的TaoShareException,就属于受检的异常。当一个方法抛出受检异常时,意思就是告诉调用者,要调用我这个方法,你必须捕获我抛出的异常。当然调用者捕获这个异常之后可以忽略,但这通常不是个好办法,最起码应该打印一条日志。
运行时异常和错误属于未受检的可抛出结构,在行为上是相同的:他们都不需要被捕获。这是因为当我们的程序抛出这两种结构时,通常代表的是该错误不可被恢复,继续执行下去有害无益,比如IndexOutOfBoundsException、ArithmeticException和OutOfMemoryError。IndexOutOfBoundsException表示数组或者字符串超出了索引范围,ArithmeticException表示运算异常(比如分母为0),OutOfMemoryError表示JVM没有足够的内存来分配。前两种异常都属于RuntimeException,表明这种异常属于编程错误,我们应该在我们的程序中就避免这种问题,而不是通过捕获来解决。后一种属于错误,是我们无法通过编码来解决的,这种错误通常都是比较严重的问题,一般是JVM出了问题。
2 Java异常处理原则
2.1 不要忽略异常
这是处理异常最基本的原则,当一个异常被抛出的时候,说明程序中某个地方出了问题,忽略异常可能达不到我们期望的效果。我们应该在程序中对出现的异常进行处理,或者是继续抛出。至少,我们应该包含一条语句,将该异常记录进日志,便于我们将来的分析。
2.2 尽量少用异常,优先使用标准异常
异常的处理会有一定的开销,除非是在出现异常的情况下,否则不要将异常用于普通的控制流。比如,不要将异常用来检测数组是否越界。如下:
//摘自《Effective Java》 try{ int i=0; while(true) range[i++].climb(); }catch(ArrayIndexOutOfBoundsException e){ } |
还有,不要总是依赖异常来解决空指针问题,我们在调用方法之前就应该判断对象是否为null。注意要判断所有需要的对象是否为null,我以前碰到一个问题就是只判断了部分为null,如下代码所示:
If(baskItem.getStatus() != null && baskItem.getStatus().getStatus() != Status.TOP.getStatus()) |
结果在运行的过程中,却出现了baskItem为null的情况。当然这种情况是很少遇到的,但是在某些极端的情况下还是会遇到。
在使用到异常的地方,优先使用标准异常而不是自定义异常,这样会使你的代码更具可读性(因为标准异常大家都熟悉),同时也减少了异常类。
常见的标准异常有:
IllegalArgumentException,这个异常表示调用者传递了不合适的参数。一般在检测到参数不正确的时候我们可以抛出这个异常,或者是返回null值或false,结束方法。
NullPointerException,这个是我们最熟悉的异常了。如果调用者在某个不允许null值的参数中传递了null值,习惯的做法就是抛出NullPointerException异常。
IndexOutOfBoundsException,如果调用者在某个序列下标的参数中传递了越界的值,应该抛出的就是IndexOutOfBoundsException异常。比如访问超过数组下标的数组元素。
ConcurrentModificationException,这个异常被设计在java.util包中,用来表示一个单线程的对象正在被并发的修改。
UnsupportedOperationException,这个异常表示当前对象不支持所请求的操作。比如在实现类中没有实现接口定义的方法,就会抛出这个异常。
NumberFormatException,这个异常表示数据格式有误,还有一个ArithmeticException异常,表示算术异常,比如在除法运算中传递了0作为除数。
还有我们使用的DAOException异常,表示访问数据库出了问题。
2.3 每个方法抛出的异常都要有文档
不仅要为每个方法建立文档,为每个方法抛出的异常建立完善的文档也是很有必要的。
始终要单独声明每个受检的异常,并且利用Javadoc的@throws标记,准确地记录产生每个异常的条件。如下所示:
/** * Create a POIFSFileSystem from an InputStream * * @param stream the InputStream from which to read the data * * @exception IOException on errors reading, or on invalid data */ public POIFSFileSystem(final InputStream stream) throws IOException; |
(注: @throws以前的写法是@ecxeption)
对于未受检异常,只要使用@throws标签记录下可能抛出的异常以及条件就可以了。但不要使用throws关键字将该异常包含在方法的声明中。这是因为使用throws关键字声明的异常必须在调用者的代码中进行捕获,而未受检的异常一般是不需要调用者捕获的。
2.4 记录所捕获到异常的有用信息
异常发生时,我们通常都会查看产生异常的原因来排查问题,所以在异常中包含尽可能有用的细节信息对我们解决问题有很大的帮助。还好,一般的异常都会记录异常产生的类名、方法名和发生异常的位置,以及大量的堆栈信息。
实际上仅有这些信息也不能完全满足我们的需求。有时候,我们通过异常能判断是操作数据库的时候出错了,但我们还不能确定是我们的SQL语句有错,还是没有操作权限,或者是数据库操作过程中发生了其它错误。这时候我们可能希望能看到执行的SQL语句或其它中间状态,记录这些信息也是很有必要的。有两种方式可以实现,一种做法是在捕获异常后再记录这些信息;另一种做法是在自定义异常类的构造器中引入这些信息。比如我们可以在IndexOutOfBoundsException异常中定义一个如下的构造器。
public IndexOutOfBoundsException(int lowerBound,int upperBound, int index){ super("Lower bound:" + lowerBound + ", Upper bound:" + upperBound + ", Index:" + index); this.lowerBound = lowerBound; this.upperBound = upperBound; this.index = index; } |
以上代码摘自《Effective Java》,虽然JDK并没有这样做,但是Joshua Bloch还是极力推荐这种做法。
其实在SQLException里是有比较详细的信息记录的,看一下SQLException的源代码就会发现它有一个如下的构造器:
public SQLException(String reason, String sqlState, int vendorCode, Throwable cause) |
这个构造器为我们详细的记录了异常发生的原因。Reason表示异常发生的原因;sqlState是XOPEN或者SQL:2003(一种规范)关于异常分类的代码;vendorCode是数据库特定的异常代码。
有了以上的信息之后,我们就可以通过SQLException中的getSQLState()和getErrorCode()方法来获得异常更精确的异常信息。
2.5 努力使失败保持原子性
失败的原子性,意思就是失败的方法调用应该使对象保持在调用之前的状态。摘自《Effective Java》
这一点听起来比较抽象,但是实际上很容易理解。试想想一种情况,当我们捕获了受检的异常之后,我们并不会终止我们的代码继续执行,而试图从这个异常中恢复。但是有一个对象的值却在异常中被改变了,这样我们的下一步操作可能取到的是一个不期望得到的值。举个例子,假如我们要统计进行了某种操作的分享,用下面的代码,或许在我们获取分享的时候就失败了,但是计数器却进行了加1,很明显不是我们期望的结果。
int count=0; for(long baskItemId:arraylist){ count++; BaskItem baskItem=this.getBaskItemById(baskItemId,buyerId); if(baskItem!=null){ …; } } |
为了避免异常所产生的对象改变,我们可以有很多预防办法。
① 在执行操作之前对参数进行校验,如果参数不对就抛出异常,避免后续的操作改变对象的值。(实际上在一个方法开始之前进行参数校验是很好的做法,不仅可以避免异常带来的对象改变,还可以避免不必要的开销。)
② 调整处理顺序,使得任何可能引起对象改变的操作在可能产生异常的调用之后。
③ 在捕获异常之后编写一段恢复的代码,进行回滚。
④ 拷贝一份对象,在对象的拷贝上执行操作。当执行完毕后用拷贝的对象替换原来的对象。
当然,并不是针对任何异常都需要保持失败的原子性,如果方法抛出了某种错误,使失败保存原子性就没有多大意义了。比如出现OutOfMemoryError,就没必要再从错误中恢复了。
2.6 不要一次捕获所有异常
在我们的代码中,经常会看到如下一劳永逸的代码。
try { …… ……//抛出AException …… ……//抛出BException …… ……//抛出CException } catch (Exception e) { log.error("XXX error", e.getCause()); } |
这段代码的特点是用一个catch子句捕获了所有的异常,看上去省事很多。实际上是存在缺陷的。一是可能针对不同的异常有不同的恢复措施,而这里的代码让我们无法区分异常,也就无法针对性的恢复。二是这种方式掩盖了代码中的RuntimeException异常,通常出现RuntimeException属于编程错误,也就是这种方式掩盖了我们的编程错误。
针对一段代码中可能有多个调用会产生异常,我们可以分别捕获每种异常,进行不同的处理。
2.7 对可恢复的情况使用受检异常,对编程错误使用运行时异常
这一点在明确了受检异常和未受检异常的区别就不难理解了。受检异常是在编程中必须捕获的异常。我们捕获异常的目的就是在异常发生时希望采取一些措施来恢复。那如果我们希望调用者在调用我们的方法出错之后能有补救措施,我们就应该抛出受检异常。而对于一些编程错误,我们希望的是调用者能够在代码中就避免,这时候就应该抛出未受检的异常RuntimeException,提示调用者改进代码。比如前面提到的IndexOutOfBoundsException异常,出现这类异常,就应该考虑在代码中避免数组越界了。
2.8 在finally块中释放资源
针对有些操作,比如IO、Socket或者数据库操作,发生异常的时候,我们还需要正确释放占用的资源。一般我们在finally块中完成资源的释放,值得注意的是在finally块中的子句也可能会出现异常,我们在编码的时候要尽量避免出现这种异常。如下所示:
try{ …… reader = new BufferedReader(new InputStreamReader( new FileInputStream(file))); for (String record = reader.readLine(); record != null { …… } } catch (FileNotFoundException e) { log.error("error while reading file " + file.getName()); } catch (IOException e) { log.error("error while reading file " + file.getName() + e); } catch (Exception e) { log.error("baskitem 任务失败…"); } finally { try { reader.close(); } catch (IOException e) { log.error("error while closeing file " + file.getName()); } } |