通过异常处理错误

概念

Java的基本理念是“结构不佳的代码不能运行”

如果在每次调用方法的时候都彻底地进行错误检査,代码很可能会变得难以阅读。对于构造大型、健壮、可维护的程序而言,这种错误处理模式将会成为主要障碍。

我们的做法应该是用强制规定的形式来消除错误处理过程中随心所欲的因素。这种做法由来已久,对异常处理的实现可以追湖到20世纪60年代的操作系统,甚至于BASIC语言中的on error goto语句。而C++的异常处理机制基于Ada,Java中的异常处理则建立在C++的基础之上(尽管看上去更像Object Pascal)。

“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理。你要停下来看着是不是有别人或在别的地方能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的坏境中,在这里将作出正确的决定。

使用异常所带来的另一个相当明显的好处是,它往往能够降低错误处理代码的复杂度。如果不使用异常,那么就必须检査特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检査,因为异常机制将保证能够捕获这个错误。并且只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的回读、编写和调试工作更加井井有条。

基本异常

异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通同题是指,在当前环境下能得到足够的信息,总能处理这个错误。 而对于异常情形就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。

当抛出异常后有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前坏境中弹出对异常对象的引用。此时异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行要么继续通行下去。

异常使得我们可以将每件事都当作一个事务来考虑,而异常可以看护着这些事务的底线,我们还可以将异常看作是一种内建的恢复(undo)系统,因为(在细心使用的情况下)我们在程序中可以拥有各种不同的恢复点。如果程序的某部分失败了,异常将“恢复”到程序中某个已知的稳定点上。

异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。

异常参数

与使用Java中的其他对象一样,我们总是用new在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器:一个是默认构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器;

throw new NullPointerException(“t == null”);

关键字throw将产生许多有趣的结果。在使用new创建了异常对象之后,此对象的引用将传给throw。尽管返回的异常对象其类型通常与方法设计的返回类型不同,但从效果上看它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,当然着过分强调这种类比的话,就会有麻烦了。另外还能用抛出异常的方式从当前的作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。

抛出异常与方法正常返回值的相似之处到此为止。因为异常返回的“地点”与普通方法调用返回的“地点”完全不同。(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层次。)

此外,能够抛出任意类型的Throwable对象,它是异常类型的根类。通常对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层坏境通过这些信息来决定如何处理异常。(通常异常对象中仅有的信息就是异常类型,除此之外不包含任何有意义的内容。)

try块

要明白异常是如何被捕获的,必须首先理解监控区域(guarded region)的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为在这个块里“尝试”各种(可能产生异常的)方法调用,所以称为try块。它是跟在try
关键字之后的普通程序块:

try {
    // Code that might generate exceptions
}

对于不支持异常处理的程序语言,要想仔细检査错误,就得在每个方法调用的前后加上设置和错误检査的代码,甚至在每次调用同一方法时也得这么做。有了异常处理机制,可以把所有动作都放在try块里,然后只需在一个地方就可以捕获所有异常。这意味着代码将更容易编写和阅读,因为完成任务的代码没有与错误检査的代码混在一起。

异常处理程序

当然,抛出的异常必须在某处得到处理。这个“地点”就是异常处理程序,而且针对每个要捕获的异常得准备相应的处理程序。异常处理程序紧跟在try块之后,以关键字catch表示:

try {
    // Code that might generate exceptions
} catch(Type1 id1) {
    // Handle exceptions of Type1
} catch(type2 ld2) {
    // Handle exceptions of Type1
}

每个catch子句(异常处理程序)看起来就像是接收一个且仅接收一个特殊类型的参数的方法。可以在处理程序的内部使用标识符(id1, id2等等),这与方法参数的使用很相似。有时可能用不到标识符,因为异常的类型已经给了你足够的信息来对异常进行处理,但标识符并不可以省略。

异常处理程序必须紧跟在try块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则处理程序的査找过程结束。注意,只有匹配的catch子句才能得到执行,这与switch语句不同,switch语句需要在每一个case后面跟一个break,以避免执行后续的case子句。

注意在try块的内部,许多不同的方法调用可能会产生类型相同的异常,而你只需要提供一个针对此类型的异常处理程序。

终止与恢复

异常处理理论上有两种基本模型。Java支持终止模型(它是Java和C++所支持的模型)。

在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。

另一种称为恢复模型。意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。如果想要用Java实现类似恢复的行为,那么在遇见错误时就不能抛出异常,而是调用方法来修正该错误。或者把try块放在while循环里,这样就不断地进入try,直到得到满意的结果。

长久以来,尽管程序员们使用的操作系统支持恢复模型的异常处理,但他们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常批出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。

创建自定义异常

不必构泥于Java中已有的异常类型。Java提供的异常体系不可能预见所有的希望加以报告的错误,所以可以自己定义异常类来表示程序中可能会遇到的特定问题。

要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生默认构造器,所以这几乎不用写多少代码:

class SimplException extends Exception{
	public SimplException() {}
	public SimplException(String str) {
		super(str);
	}
}
public class InheritingExceptions {
	public void f() throws SimplException{
		System.out.println("Throw SimplException from f()");
	}
	public void f2() throws SimplException{
		throw new SimplException("主动抛出异常");
	}
	public static void main(String[] args) {
		InheritingExceptions i = new InheritingExceptions();
		try {
			i.f();
			i.f2();
		} catch (Exception e) {
			e.printStackTrace(System.out);
		}
	}
	//输出:
	//Throw SimplException from f()
	//thinkinjava.SimplException: 主动抛出异常
	//at InheritingExceptions.f2(InheritingExceptions.java:15)
	//at InheritingExceptions.main(InheritingExceptions.java:21)
}

两个构造器定义了 SimplException 类型对象的创建方式。对于第二个构造器,使用super关键字调用了基类构造器,它接受一个字符串作为参数。

异常与记录日志
你可能还想使用java.util.logging工具将输出记录到日志中。

class LoggingException extends Exception{
	private static Logger logger = Logger.getLogger("LoggingException");
	public LoggingException() {
		StringWriter sw = new StringWriter();
		printStackTrace(new PrintWriter(sw));
		logger.severe(sw.toString());
	}
}

public class InheritingExceptions {
	public static void main(String[] args) {
		try {
			throw new LoggingException();
		} catch (Exception e) {
			System.err.println(e);
		}
	}
	//输出:
	//月 24, 2018 8:53:28 上午 thinkinjava.LoggingException <init>
	//SEVERE: thinkinjava.LoggingException
	//at thinkinjava.InheritingExceptions.main(InheritingExceptions.java:32)
	//thinkinjava.LoggingException

}

异常说明

Java鼓励人们把方法可能会抛出的异常告知使用此方法的容户端程序员。这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。当然,如果提供了源代码,客户端程序员可以在源代码中査找throw语句来获知相关信息,然而程序库通常并不与源代码一起发布。为了预防这样的问题,Java提供了相应的语法(并强制制使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。

异常说明使用了附加的关键字throws,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:

void f() throws TooBig,TooSmall,DivZero{ //…

但是,要是这样写:

void f() { //…

就表示此方法不会抛出任何异常(除了从RuntimeException继承的异常,它们可以在没有异常说明的情况下被抛出,这些将在后面进行讨论)。

代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你,要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java在编译时就可以保证一定水平的异常正确性。

不过还是有个能“作弊”的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。

这种在编译时被强制检査的异常称为被检查的异常

捕获所有异常

可以只写一个异常处理程序来捕获所有类型的异常。通过捕获异常类型的基类Exception就可以做到这一点(事实上还有其他的基类,但Exception是同编程活动相关的基类):

catch(Exception e1) {
    System.out.println(“Caught an exception”);
}

这将捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常捕获了。因为Exception是与编程有关的所有异常类的基类,所以它不会含有太多具体的信息,不过可以调用它从其基类Throwable继承的方法:

String getMessage()
String getLocalizedMessage()

用来获取详细信息,或用本地语言表示的详细信息。

String toString()

返回对Throwable的简单描述,要是有详细信息的话,也会把它包含在内。

void printStackTrace()
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)

打印Throwable和Throwable的调用栈轨迹。调用栈显示了“把你带到异常抛出地点”的方法调用序列。其中第一个版本输出到标准错误,后两个版本允许选择要输出的流。

Throwable fillInStackTrace()

用于在Throwable对象的内部记录栈帧的当前状态,这在程序重新抛出错误或异常时很有用。

栈轨迹

PrintStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。元素0是栈顶元素并且是调用序列中的最后一个方法调用(这个Throwable被创建和抛出之处)。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。下面是个例子:

public class WhoCalled {
	static void f(){
		try {
			throw new Exception();
		} catch (Exception e) {
			e.printStackTrace();
			for(StackTraceElement ste:e.getStackTrace()){
				System.out.println(ste.getMethodName());
			}
		}
	}
	static void g(){f();}
	static void h(){g();}
	public static void main(String[] args) {
		h();
	}
	//输出:
	//java.lang.Exceptionf
	//g
	//h
	//main
	//...
}

重新抛出异常

有时希望把刚捕获的异常重新抛出,尤其是在使用Exception捕获所有异常的时候。既然已经得到了对当前异常对象的引用,可以直接把它重新抛出:

catch(Exception e) {
    System.out.println(“An exception was thrown”);
    throw e:
}

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将被忽略。此外异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStacktrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的,就像这样:

public class WhoCalled {
	static void f() throws Exception{
		throw new Exception("throw from f()");
	}
	static void g() throws Exception{
		f();
	}
	static void h()throws Exception{
		try {
			g();
		} catch (Exception e) {
			throw (Exception)e.fillInStackTrace();
			//throw e;
		}
	}
	public static void main(String[] args) {
		try {
			h();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	//输出:
	//java.lang.Exception: throw from f()
	//at thinkinjava.WhoCalled.h(WhoCalled.java:14)
	//at thinkinjava.WhoCalled.main(WhoCalled.java:19)
	
	//如果使用throw e;抛出则输出:
	//java.lang.Exception: throw from f()
	//at thinkinjava.WhoCalled.f(WhoCalled.java:5)
	//at thinkinjava.WhoCalled.g(WhoCalled.java:8)
	//at thinkinjava.WhoCalled.h(WhoCalled.java:12)
	//at thinkinjava.WhoCalled.main(WhoCalled.java:20)
}

异常链

常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。在JDK1.4以前,程序员必须自己编写代码来保存原始异常的信息。现在所有Throwable的子类在构造器中都可以接受一个cause(因由)对象作为参数。这个cause就用来表示原始异常,这样通过把原始异常传进给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。

有趣的是,在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器。它们是Error(用于Java虚拟机报告系统错误)、Exception以及RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause()方法而不是构造器。
例:

public class WhoCalled2 {
	static void f(){
		try {
			throw new Exception();
		} catch (Exception e) {
			RuntimeException re = new RuntimeException();
			re.initCause(e);
			throw re;
		}
	}

	public static void main(String[] args) {
		f();
	}
	//输出:
	//Exception in thread "main" java.lang.RuntimeException
	//		at thinkinjava.WhoCalled2.f(WhoCalled2.java:9)
	//		at thinkinjava.WhoCalled2.main(WhoCalled2.java:16)
	//Caused by: java.lang.Exception
	//		at thinkinjava.WhoCalled2.f(WhoCalled2.java:7)
	//		... 1 more

}

Java标准异常

Throwable这个Java类被用来表示任何可以作为异常被抛出的类。Throwable对象可分为两种类型(指从Throwable继承而得到的类型):Error用来表示编译时和系统错误(除特殊情况外,一般不用你关心),Exception是可以被抛出的基本类型,在Java类库、用户方法以及运行时故障中都可能抛出Exception型异常。所以Java程序员关心的基类型通常是Exception。

要想对异常有全面的了解,最好去浏览一下HTML格式的Java文档(可以从java.sun.com下载)。为了对不同的异常有个感性的认识,这样做是值得的。但很快你就会发现,这些异常除了名称外其实都差不多。同时,Java中异常的数目在持续增加,所以在这里罗列它们毫无意义。所使用的第三方类库也可能会有自己的异常。对异常来说,关键是理解概念以及如何使用。

异常的基本的概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。异常并非全是在java.lang包里定义的。有些异常是用来支持其他像util、net和io这样的程序包,这些异常可以通过它们的完整名称或者从它们的父类中看出端倪。比如,所有的输入/输出异常都是从
java.io.IOException继承而来的。

特例RuntimeException

在上文中你会发现这样一个异常:

throw new NullPointerException();

如果必须对传递给方法的每个引用都检査其是否为null(因为无法确定调用者是否传入了非法引用),这听起来着实吓人。幸运的是,这不必由你亲自来做,它属于Java的标准通行时检测的一部分。如果对null引用进行调用,Java会自动抛出NullPointerException异常,所以上述代码是多余的,尽管你也许想要执行其他的检査以确保NullPointerException不会出现。

属于运行时异常的类型有很多,它们会自动被Java虚拟机抛出所以不必在异常说明中把它们列出来。这些异常都是从RuntimeException类继承而来,所以既体现了继承的优点,使用起来也很方便。这构成了一组具有相同特征和行为的异常类型。并且,也不再需要在异常说明中声明方法将抛出RuntimeException类型的异常(或者任何从RuntimeException继承的异常),它们也被称为“不受检查异常”。这种异常属于错误,将被自动捕获,就不用你亲自动手了。要是自己去检査RuntimeExcePtion的话,代码就显得太混乱了。不过尽管通常不用捕获
RuntimeExcePtion异常,但还是可以在代码中抛出RuntimeException类型的异常。

请务必记住:只能在代码中忽略RuntimeException(及其子类)类型的异常,其他类型异常的处理部是由编译器强制实施的。究其原因,RuntimeException代表的是编程错误:

  1. 无法预料的错误。比如从你控制范围之外传递进来的null引用。
  2. 作为程序员,应该在代码中进行检査的错误。

你会发现在这些情况下使用异常很有好处, 它们能给调试带来便利。

值得注意的是:不应把Java的异常处理机制当成是单一用途的工具。是的,它被设计用来处理一些烦人的运行时错误,这些错误往往是由代码控制能力之外的因素导致的,然而它对于发现某些编译器无法检测到的编程错误也是非常重要的。

使用finally进行清理

对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。这通常适用于内存回收之外的情况(因为回收由垃圾回收器完成)。为了达到这个效果,可以在异常处理程序后面加上finally语句。完整的异常处理程序看起来像这样:

try {
    // The guarded region: Dangerous activities
} catch(A a1){
    // Handler for situation A
} catch(B b1) {
    // Handler for situation B
} finally {
    // Activities that happen every time

对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它能使程序员保证:无论try块里发生了什么,内存总能得到释放。但Java有垃圾回收机制,所以内存解放不再是问题。而且Java也没有析构函数可供调用。那么,Java在什么情况下才能用到finally呢?

当要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关,如下面例子所示:

public class OnOff {

	private boolean state = false;
	public void on(){state = true;}
	public void off(){state = false;}
	@Override
	public String toString() {
		return state?"on":"off";
	}
	public static void main(String[] args) {
		OnOff oo = new OnOff();
		try {
			oo.on();
			//do something
			throw new Exception();
		} catch (Exception e) {
			System.out.println(oo);
		}finally{
			oo.off();
			System.out.println(oo);
		}
	}
	//输出:
	//on
	//off
}

上面的例子表示无论如何oo对象的某个开关在结束或抛出异常之后都必须保证关闭状态。

当涉及break和continue语句的时候,finally子句也会得到执行。请注意,如果把finally子句和带标签的break及continue配合使用,在Java里就没必要使用goto语句了。

在return中使用finally

因为finally子句总是会执行的,所以在一个方法中,可以从多个点返回,并且可以保证重要的清理工作仍旧会执行

public class Test{
	public static void f(int i){
		try {
			System.out.println("1");
			if(i == 1) return;
			System.out.println("2");
			if(i == 2) return;
			System.out.println("end");
		} finally {
			System.out.println("clear");
		}
	}
	public static void main(String[] args) {
		f(2);
	}
	//输出:
	//1
	//2
	//clear

}

从输出中可以看出,在finally类内部,从何处返回无关紧要。

缺憾:异常丢失

遗憾的是,Java的异常实现也有瑕疵。异常作为程序出错的标志,决不应该被忽略,但它还是有可能被轻易地忽略。用某些特殊的方式使用finally子句,就会发生这种情况。下面是一个简单的异常丢失例子:

public class Test {
	public static void main(String[] args) {
		try {
			throw new RuntimeException();
		} finally {
			return;
		}
	}
}

如果运行这个程序,就会看到即使抛出了异常,它也不会产生任何输出。

构造器

有一点很重要,即你要时刻询问白己“如果异常发生了,所有东西能被正确的清理吗?”尽管大多数情况下是非常安全的,但涉及构造器时,问题就出现了。构造器会把对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理。如果在构造器内抛出了异常,这些清理行为也许就不能正常工作了。这意味着在编写构造器时要格外细心。

也许使用finally就可以解决问题。但问题井非如此简单,因为finally会每次都执行清理代码。如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在finally子句中却是要被清理的。

异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,当基类使用的代码应用到其派生类对象的时候,一样能够工作(当然,这是面向对象的基本概念),异常也不例外。

异常限制对构造器不起作用。因为基类构造器必须以这样或那样的方式被调用,派生类构造器的异常说明必须包含基类构造器的异常说明。派生类构造器不能捕获基类构造器抛出的异常

尽管在继承过程中,编译器会对异常说明做限制要求,但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的装型组成的。因此不能基于异常说明来重载方法。此外,一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里。这点同继承的规则明显不同,在继承中基类的方法必须出现在派生类里,换句话说,在继承和覆盖的过程中,某个特定方法的“异常说明的接口不是变大了而是变小了——这恰好和类接口在继承时的情形相反。(注:此处应该贴出代码…)

异常匹配

抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续査找。

査找的时候井不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序,就像这样:

class Aa extends Exception{}
class Bb extends Aa{}

public class Sneeze {
	public static void main(String[] args) {
		try {
			throw new Bb();
		} catch (Bb b) {
			System.out.println("B");
		} catch (Aa a) {
			System.out.println("A");
		}
		try {
			throw new Bb();
		} catch (Aa a) {
			System.out.println("A");
		}
	}
	//输出:
	//B
	//A
}

Bb异常会被第一个匹配的catch子句捕获,也就是程序里的第一个。然而如果将这个catch子句删掉,只留下Aa的catch子句,该程序仍然能运行,因为这次捕获的是Aa的基类。换句话说,catch(Aa)会捕获Aa以及所有从它派生的异常。这一点非常有用,因为如果决定在方法里加上更多派生异常的话,只要客户程序员捕获的是基类异常,那么它们的代码就无需更改。

如果把捕获基类的catch子句放在最前面,以此想把派生类的异常全给“屏蔽”掉,就像这样:
try{
    throw new Aa();
}catch(Aa a){
    //…
}catch(Bb b){
    //…
}
这样编译器就会发现Bb 的一catch子句永远也得不到执行,因此它会向你报告错误。

其他可选方式

异常处理系统就像一个活门(trap door),使你能放弃程序的正常执行序列。当“异常情形发生的时候,正常的执行已变得不可能或者不需要了,这时就要用到这个“活门”。异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽略。应该注意到,开发异常处理的初衷是为了方便程序员处理错误。

异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分高。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样以来,主干代码就不会与错误处理逻辑混在一起,也更容易理解和维护。通过允许一个处理程序去处理多个出错点,异常处理还使得错误处理代码的数量趋向于减少。

“被检査的异常”使这个问题变得有些复杂,因为它们强制你在可能还没准备好处理错误的时候被追加上catch子句,这就导致了吞食则有害(harmful swallowed)的问题:

try{
    //…to do something useful
}catch(ObligatoryException e){}

程序员们只做最简单的事情,常常是无意中“吞食”了异常,然而一旦这么做,虽然能通过编译,但除非你记得复査并改正代码,否则异常将会丢失。异常确实发生了,但“吞食”后它却完全消失了。因为编译器强制你立刻写代码来处理异常,所以这种看起来最简单的方法,却可能是最糟糕的做法。

这个话题看起来简单,但实际上它不仅复杂,更重要的是还非常多变。总有人会顽固地坚持自己的立场,声称正确答案(也是他们的答案)是显而易见的。我觉得之所以会有这种观点,是因为我们使用的工具已经不是ANSI标准出台前的像C那样的弱类型语言,而是像C++和Java这样的“强静态类型语言”(也就是编译时就做类型检査的语言),这是前者所无法比拟的。当刚开始这种转变的时候,会觉得它带来的好处是那样明显,好像类型检査总能解决所有的问题。在此,我想结合我自已的认识过程,告诉读者我是怎样从对类型检査的绝对迷信变成持怀疑态度的,当然,很多时候它还是非常有用的,但是当它挡住我们的去路并成为障碍的时候,我们就得跨过去。只是这条界限往往并不是很清晰(我(作者)最喜欢的一句格言是:所有模型都是错误的,但有些是能用的)。

把“被检查的异常”转换为“不检查的异常”

当在一个普通方法里调用别的方法时,要考虑到“我不知道该这样处理这个异常,但是也不想把它‘吞’了,或者打印一些无用的消息”。JDK 1.4的异常链提供了一种新的思路来解决这个问题。可以直接把“被检査的异常”包装进RuntimeException里面,就像这样:

try{
    //…to do something useful
}catch(IDontKnowWahtToDoWithThisCheckException e){
    throw new RuntimeException(e);
}

如果想把“被检査的异常”这种功能“屏蔽”掉的话,这看上去像是一个好办法。不用“吞下”异常,也不必把它放到方法的异常说明里面,而异常链还能保证你不会丢失任何原始异常的信息 。 这种技巧给了你一种选择,你可以不写try-catch子句和/或异常说明,直接忽略异常,让它自己沿着调用栈往上“冒泡”。同时,还可以用getCause()捕获并处理特定的异常,就像这样:

class WrapCheckedExcetion{
	void f(){
		try {
			throw new IOException();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

public class Sneeze {
	public static void main(String[] args) {
		WrapCheckedExcetion w = new WrapCheckedExcetion();
		try {
			w.f();
		} catch (Exception e) {
			try {
				throw e.getCause();
			} catch (IOException e1) {
				e1.printStackTrace();
			} catch (Throwable e1) {
				e1.printStackTrace();
			}
		}
	}
	//输出:
	//java.io.IOException
	//	at thinkinjava.WrapCheckedExcetion.f(Sneeze.java:8)
	//	at thinkinjava.Sneeze.main(Sneeze.java:19)
}

上面程序的大意是,定义了一个方法能抛出异常,且把异常捕获后包装进RuntimeException对象再抛出;在main()里调用上述方法并捕获到它抛出的RuntimeException异常后,用getCause()方法把被包装的原始异常提取出来,再用它们自己的catch子句进行处理。

总结

应该在下列情况下使用异常:

  1. 在恰当的级別处理问题。(在知道该如何处理的情况下才補获异常。)
  2. 解决问题并且重新调用产生异常的方法。
  3. 进行少许修补,然后绕过异常发生的地方继续执行。
  4. 用别的数据进行计算,以代替方法预计会返回的値。
  5. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
  6. 把当前运行坏境下能做的事情尽量做完,然后把不同的异常抛到更高层。
  7. 终止程序。
  8. 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛著也很烦人。)
  9. 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮做长期投资。)

异常是Java程序设计不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一处处理你编写的这段代码中产生的错误。尽管异常通常被认为是一种工具,使得你可以在运行时报告错误并从错误中恢复,但是我一直怀疑到底有多少时候“恢复”真正得以实现了,或者能够得以实现


  1. 本文来源《Java编程思想(第四版)》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值