通过异常处理错误(4):java标准异常、使用finally进行清理

一、java标准异常

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

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

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

二、特例:RuntimeException

    在本章第一个例子中:

if(t == null)
    throw new NullPointerException();

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

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

    如果不捕获这种类型的异常会发生什么事呢?因为编译器没有在这个问题上对异常说明进行强制检查,RuntimeException类型的异常也许会穿越所有的执行路径直达main()方法,而不会被捕获。要明白到底发生了什么,可以试试下面的例子:

public class NeverCaught {
	static void f() {
		throw new RuntimeException("From f()");
	}

	static void g() {
		f();
	}

	public static void main(String[] args) {
		g();
	}
}

    可能你已经发现,RuntimeException(或任何从它继承的异常)是一个特例。对于这种异常类型,编译器不需要异常说明,其输出被报告给了System.err:

    所以答案是:如果RuntimeException没有被捕获而直达main(),那么程序退出前将调用异常的printStackTrace()方法。

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

  1. 无法预料的错误。比如从你控制范围之外传递进来的null引用。
  2. 作为程序员,应该在代码中进行检查的错误。(比如对于ArrayIndexOutOfBoundsException,就得注意一下数组的大小了。)在一个地方发生的异常,常常会在另一个地方导致错误。

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

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

三、使用finally进行清理

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

try {

} catch(A a1) {

} catch(B b1) {

} catch(C c1) {

} finally {

}

    为了证明finally子句总能运行,可以试试这个程序:

class ThreeException extends Exception {
}

public class FinallyWorks {
	static int count = 0;

	public static void main(String[] args) {
		while (true) {
			try {
				if (count++ == 0)
					throw new ThreeException();
				System.out.println("No exception");
			} catch (ThreeException e) {
				System.out.println("ThreeException");
			} finally {
				System.out.println("In finally clause");
				if (count == 2)
					break;
			}
		}
	}
}

    可以从输出中发现,无论异常是否被抛出,finally子句总能被执行。

    这个程序也给了我们一些思路,当java中的异常不允许我们回到异常抛出的地点时,那么该如何应对呢?如果把try块放在循环里,就建立了一个“程序继续执行之前必须要达到”的条件。还可以加入一个static类型的计数器或者别的装置,使循环在放弃以前能尝试一定的次数。这将使程序的健壮性更上一个台阶。

四、finally用来做什么

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

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

public class Switch {
	private boolean state = false;

	public boolean read() {
		return state;
	}

	public void on() {
		state = true;
		System.out.println(this);
	}

	public void off() {
		state = false;
		System.out.println(this);
	}

	public String toString() {
		return state ? "on" : "off";
	}
}
public class OnOffException1 extends Exception {
}
public class OnOffException2 extends Exception {
}
public class OnOffSwitch {
	private static Switch sw = new Switch();

	public static void f() throws OnOffException1, OnOffException2 {
	}

	public static void main(String[] args) {
		try {
			sw.on();
			f();
			sw.off();
		} catch (OnOffException1 e) {
			System.out.println("OnOffException1");
			sw.off();
		} catch (OnOffException2 e) {
			System.out.println("OnOffException2");
			sw.off();
		}
	}
}

    程序的目的是要确保main()结束的时候开关必须是关闭的,所以在每个try块和异常处理程序的末尾都加入了对sw.off()方法的调用。但也可能有这种情况:异常被抛出,但没有被处理程序捕获,这时sw.off()就得不到调用。但是有了finally,只要把try块中的清理代码移放在一处即可:

public class WithFinally {
	static Switch sw = new Switch();

	public static void main(String[] args) {
		try {
			sw.on();
			OnOffSwitch.f();
		} catch (OnOffException1 e) {
			System.out.println("OnOffException1");
		} catch (OnOffException2 e) {
			System.out.println("OnOffException2");
		} finally {
			sw.off();
		}
	}
}

    这里sw.off()被移到一处,并且保证在任何情况下都能得到执行。

    甚至在异常没有被当前的异常程序捕获的情况下,异常处理机制也会在跳到更高一层的异常处理程序之前,执行finally子句:

class FourException extends Exception {
}

public class AlwaysFinally {
	public static void main(String[] args) {
		System.out.println("Entering first try block");
		try {
			System.out.println("Entering second try block");
			try {
				throw new FourException();
			} finally {
				System.out.println("finally in 2nd try block");
			}
		} catch (FourException e) {
			System.out.println("Caught FourException in 1st try block");
		} finally {
			System.out.println("finally in 1st try block");
		}
	}
}

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

五、在return中使用finally

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

public class MultipleReturns {
	public static void f(int i) {
		System.out.println("Initialzation that requires cleanup");
		try {
			System.out.println("point 1");
			if (i == 1)
				return;
			System.out.println("point 2");
			if (i == 2)
				return;
			System.out.println("point 3");
			if (i == 2)
				return;
			System.out.println("End");
			return;
		} finally {
			System.out.println("Performing cleanup");
		}
	}

	public static void main(String[] args) {
		for (int i = 1; i < 5; i++) {
			f(i);
		}
	}
}

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

六、缺憾:异常丢失

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

class VeryImportantException extends Exception {
	public String toString() {
		return "A very important exception";
	}
}

class HoHumException extends Exception {
	public String toString() {
		return "A trivial exception";
	}
}

public class LostMessage {
	void f() throws VeryImportantException {
		throw new VeryImportantException();
	}

	void dispose() throws HoHumException {
		throw new HoHumException();
	}

	public static void main(String[] args) {
		try {
			LostMessage lm = new LostMessage();
			try {
				lm.f();
			} finally {
				lm.dispose();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

    从输出中可以看到,VeryImportantException不见了,它被finally子句里的HoHumException所取代。这是相当严重的缺陷,因为异常可能会以一种比前面例子所示更微妙和难以察觉的方式完全丢失。相比之下,C++把“前一个异常还没处理就抛出下一个异常”的情形看成是糟糕的编程错误。也许在java的未来版本中会修正这个问题(另一方面,要把所有抛出异常的方法,如上例中的dispose()方法,全部打包放到try-catch子句里面)。

    一种更加简单的丢失异常的方式是从finally子句中返回:

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

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

如果本文对您有很大的帮助,还请点赞关注一下。

发布了112 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104491192