Work Better Than Yesterday!

zhangge's stupid and messy life


Home| Life| Technique Concentrate On One Thing.

Java系列之异常

01 Apr 2013

1 理解异常

Java是强静态类型语言,意思是由编译器来强制检查类型;基于这个思想,我们是想尽可能的把发现错误的时机设置为编译阶段,因为这样我们就能尽早发现错误,程序健壮性强,不容易崩溃;当然还是会有很多错误只有在运行期才能发现的。因为Java的设计目标之一是创建为他人所用的构件,那么我们在创建的时候就会考虑别人使用的时候会发生什么错误,例如参数,环境导致的错误,如果我们的构件能告诉使用者可能会发生什么错误,那么使用者就能够在使用的时候处理这些可能会发生的错误。这就是异常,它是受编译器检查的(checked exception)。

有了异常机制,我们就不用在每一步执行的时候都需要检查,专注于编写逻辑代码即可,当发生错误的时候,会在特定的异常处理程序上进行处理,或是停止执行,或是恢复继续执行,或是往外继续通知异常。这样我们就分离了业务逻辑代码和异常处理代码,代码的可读性会更高一些。

2 使用异常

因为编写代码的时候,异常的代码无处不在,所以其实使用很简单,也很熟悉。

2.1 捕获异常

可能发生异常的代码叫做监控区域,我们用try{}包围起来,意思是我去尝试执行这部分代码,然后捕获可能发生的异常,并做处理,如下:

try {
	//do something here
} catch(FileNotFoundException e1) {
	// handle here
} catch(NullPointerException e2) {
	// handle here
}

如果catch到一个异常以后,我们想要修正错误,并恢复执行,那么必须回到发生错误的那一地点,这样try里面的代码就不应该多,否则,我们得使用循环来实现恢复模型,并且像事务一样,明显,这样的代码并不优雅可读。

2.2 声明异常

第一点所说了,为了告诉别人去检查异常,那么我们就必须在方法处声明可能发生那些异常,就算不一定会发生也是正确的。如下:

public void testMethod() throws FileNotFoundException;

2.3 抛出异常

使用关键字throw即可抛出一个异常,关键字就如new,return一样,例如下面使用:

throw new NullPointerException(“object is null”);

创建一个异常对象是很快的,但是通过throw以后就不一样了,jvm需要中断程序,保护现场,获取栈信息等等的操作,可谓开销是非常大的,学过计算机组成原理我们就清楚这一切了,所以,一般情况,我们能不用异常还是好的,为了性能这一点,但是如果为了代码的优雅。。。

3 自定义异常

如果我们要自定义一个异常,通常是继承Exception类,或者别的我们想继承的异常类即可,然后定义一个构造方法即可,通常是传递错误信息给父类,之后不用做别的事情了,因为异常实际上只是一个对象而已,更多底层的事情交给了jvm和异常框架去处理,我们做要做到自定义的异常读名知其意即可。例如:

public class DIYException extends Exception {
	private static final long serialVersionUID = -2754596111657002191L;
	public DIYException(String msg) {
		super(msg);
	}
}

因为他是实现了Serializable接口的,可序列化的,所以需要生成一个ID即可。

4 高阶理解

由上面可以知道异常其实很简单的,但是要用好它,还是需要知道一些高阶的东西。

异常的根类Throwable,它代表了一切可以的异常错误,实现的了序列化接口。底下的子类有三个:

  • Error,用来表示编译时的错误和系统错误,它是不受检查的,底下有很多子类,但是我们都不用关心的。
  • StackRecorder,目前并不知道干嘛用的,顾名思义,也许是栈的记录器,也没有发现有子类。
  • Exception,它是异常的基类,底下处理RuntimeException,都是受检查的异常。

4.1 RuntimeException

这是运行时的异常错误,虽然它和别的Exception子类在代码上没多大的区别,但是实际上在异常机制框架里面,它是有很大的区别的。什么是运行时错误,例如,空指针(NullPointerException),超出索引(IndexOutOfBoundsException),分母为0(ArithmeticException),等等,只要是在运行的时候才能发现的就是;因此它也是不受检查的异常,即使你在方法上声明了,调用方法的时候编译不会去检查你有没有捕获这个异常,当然你是可以主动去捕获的。对于运行时的错误,我们最好是在代码上检查,而不是捕获它,因为那样的性能开销很大。

4.2 关于catch

由2.1知道用catch可以捕获到异常,并且多个连续一起都可以,它并不像switch语句一样会全部执行,它只是一个分支,而且是像if那样的顺序下来的,因此我们需要把上层的父类异常放到后面。如果我们不能处理异常,可以把他的错误信息打印出来,下面的方法:

e.printStackTrace();

它将打印从方法调用处直到异常抛出处的方法调用序列;因此通过这些栈信息,我们很容易定位到发生异常的地方。这个方法有多个重载的,用于把信息重定向到不同的输出流。当调用这个方法以后,程序就会被中止了的。一般我们不会调用这个方法,而是使用日志框架把异常的栈信息打印的。通过e.getMessage()即可获取栈信息。

当然,如果我们捕获到这个异常,自己处理不了,是可以直接抛出去交给别的调用者来处理的,这个时候需要在自己的方法处声明这个异常,然后在catch那里做一些日志记录工作,再throw这个异常。如果不是直接继续抛出这个异常,而是调用fillInStackTrace()方法,如下:

throw (Exception)e.fillInStackTrace();

那么它是返回一个Throwable对象,有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息。

有时候我们在catch到一个异常以后,将它封装成为自定义的异常,然后在继续抛出,那么原来的异常就成为自定义异常的一个cause了,在自定义异常的时候,可以重载构造方法,传递这个cause下去,也可以调用initCause()方法来设置cause。这个我们称作为异常链。

4.3 finally子句

如果我们的代码在某一行发生了异常,并且被捕获了,那么就会直接跳到catch子句了,原本后面的代码就无法执行了,若然我们又有一些必须执行的代码,那么我们就可以放到finally子句去执行了,例如这样:

try {
	//do something here
} catch(FileNotFoundException e1) {
	// handle here
} catch(NullPointerException e2) {
	// handle here
} finally {
	// must do here
}

通常,finally用来做什么?一般是用于清理资源,恢复到初始的状态,如已经打开的文件或者网络连接,在屏幕画的图形,数据库连接,甚至是自定义的某些开关之类的。就算之前执行了continue, break, return的语句,一样是会执行finally子句的。

4.4 关于异常声明

在上面的2.2讲了异常在方法的声明,然而异常声明和返回值那样,都不属于方法类型的一部分,方法类型依然是名字和参数组成的。当是派生的时候会是什么情况呢?

子类中重载的方法抛出的异常范围不能大于父类中方法抛出的异常的范围,即子类方法的异常只能是父方法的异常或者是该异常的子类;子类可以不抛出异常也满足该原则。

为什么?显然是父类方法的异常规定了抛出异常的最大类型,所有子类重载的方法都不能抛出比这个更大范围的异常了,只能或是子类;因为如果子类向上转型的时候,调用方法时候抛出一个比父类方法声明更大的异常,这个异常显然是不受检查的,那么哪里还满足编译器强制检查捕获的原理。

同理运用到接口,如果一个类同时继承和实现接口,在基类和接口中有同一个方法,那么就逐一检查这个原则即可。

然而,这个原则对RuntimeException无效,感觉就像所有Exception子类(除RuntimeException外,它也是Exception的子类)的范围都大于任何RuntimeException。只要任何一方,基类,接口,或者子类声明的是RuntimeException,就不会满足该原则。

为什么?上面所说了,要满足这个原则的原因就是要符合受检查的机制,那RuntimeException本来就是不受检查的异常,显然就不会符合这个原则了。


Sunday don't come easily! Subscribe to RSS Feed