七、异常和断言:
1. 异常处理:
1) 异常规范表示对于"已检查"(checked)异常,如FileNotFoundException等,既在程序运行期间可以预测到的逻辑问题引发的异常,对于该类异常,需要在包含该异常的函数声明部分标识出来,该函数可能会引发此类异常,如:
public Image loadImage(String s) throws IOException, MalformedURLException
如果在loadImage中仍然存在其他"已检查",但是没有在函数的异常规范中声明出来,那么将会导致编译失败,因此对于函数中所有"已检查"必须按照Java异常规范的要求,在函数的声明中予以标识。对于该函数(loadImage)的调用者而言,在调用该函数时,必须将其放入try块中,同时在catch字句中捕捉异常规范中标识的异常或他们的超类。
对于"运行时异常"(unchecked or runtime),由于大多是程序Bug或JVM问题所致,因此不可预测性极强,如ArrayIndexOutOfBoundException,对于该类异常无需在函数的异常规范部分予以声明。
在C++标准中也同样存在异常规范的说法,如
File* loadFile(const char* s) throw std::bad_error
所不同的是C++没有明确的要求如果函数内部抛出了该异常,则必须在函数声明的异常规范部分予以声明,对于函数调用者而言也同样没有这样的规定,必须捕获其中的异常,因此异常规范在目前的C++编译器中只是一种提示性的声明。Java和C++在异常规范方面还存在的另一个区别是,C++中,如果函数没有throw字句,那么该函数仍然可以抛出任何异常,但是对于Java的"已检查"(checked)异常则必须通过throws字句声明。
2) 如何抛出异常,在此方面,Java和C++没有太大的差异,唯一的不同是Java抛出的异常必须是Throwable的实现类,C++中则没有这样的限制,也不存在这样的异常接口。见如下代码:
2 if (someErrorOccurred)
3 //1. 通过throw关键字直接抛出指定异常类型的对象即可。
4 throw new MyCheckedException();
5 }
3) 异常捕捉:由于Java中所有的异常均继承自Throwable,所以catch(Throwable e)可以捕捉所有类型的异常,无论该异常是否为checked or unchecked异常,但是C++中并不存在这样的异常祖先接口,因此如果想达到这样的效果需要使用catch(...)关键字,这同样表示捕捉所有的异常。
4) 异常链:当异常第一次抛出并且被catch住的时候,catch块中的代码可以再次抛出捕捉到的异常,同时也可以为了使上层业务逻辑能够得到更加清晰的判断,在第一次捕捉到异常后重新定义一个新的异常并再次抛出。有的时候,如果上层逻辑在需要的时候依然可以看到原始异常,将会对错误的处理更加合理。在Java中可以通过异常链的方式达到这样的效果,见如下代码:
2 try {
3 FileInputStream in = new FileInputStream("myfile");
4 } catch (FileNotFoundException e) {
5 //定义了新的,准备再次被抛出的异常对象。
6 Throwable te = new MyCustomizedFileException("access file error.");
7 //将原始异常链接到该异常对象的内部,以供之后需要时通过getCause()方法重新获取。
8 te.initCause(e);
9 throw te;
10 }
11 }
12
13 public static void main(String[] args) {
14 try {
15 testExceptionChain();
16 } catch (MyCustomizedFileException e) {
17 //获取该异常对象的原始异常。
18 Throwable te = e.getCause();
19 System.out.println(te.getClass().getName());
20 }
21 }
22 /* 输出结果如下:
23 FileNotFoundException
24 */
5) finally字句:在Java的异常机制中存在finally这样的关键字,其块中的代码无论异常是否发生都将会被执行,从而可以确保函数内部分配或者打开的资源都能在函数内部进行释放或者关闭,如Socket连接、DB连接,见如下代码:
2 InputStream in = null;
3 try {
4 in = new FileInputStream("myfile");
5 } catch (IOException e) {
6 //TODO: do something for this exception.
7 } finally {
8 in.close();
9 }
10 //Do the following code.
11 }
在以上的代码中,无论try块中异常是否发生,finally块中的代码"in.close()" 都将会在函数退出之前或catch处理之后被执行,从而保证了FileInputStream对象能够在函数退出之前被关闭。然而这样的做法仍然可能导致一些影响代码流程的问题,如果try块中的代码没有产生异常,而是在finally中的in.close引发了异常,那么整个try{}catch{}finally{}代码块之后的代码将不会被执行,而是直接退出该函数,同时抛出in.close()引发的异常给该函数的调用者。修正代码如下:
2 InputStream in = null;
3 try {
4 in = new FileInputStream("myfile");
5 } catch (IOException e) {
6 //TODO: do something for this exception.
7 } finally {
8 try {
9 in.close();
10 } catch (IOException e) {
11 }
12 }
13 //Do the following code.
14 }
在C++中,由于对象是可以在栈上声明并且分配空间的,当栈退出后会自行调用该对象的析构函数,因此该对象的资源释放代码可以放在类的析构函数中。该方式对于一个多出口的函数而言也是非常有效的,特别是对于加锁和解锁操作需要在同一个函数中完成,为了防止在某个退出分支前意外的漏掉解锁操作,可以采用该技巧,见如下代码:
2 class ScopedLock {
3 public:
4 ScopedLock(T& lock) : _lock(lock) {
5 _lock.lock();
6 }
7
8 ~ScopedLock() {
9 _lock.unlock();
10 }
11 private:
12 LockT _lock;
13 };
14
15 void testFunc() {
16 ScopedLock s1(myLock);
17 if (cond1) {
18 return;
19 } else if (cond2) {
20 //TODO: do something
21 return;
22 } else {
23 //TODO: do something
24 }
25 return;
26 }
对于以上代码,无论函数从哪个分支退出,s1的析构函数都将调用,因此myLock的解锁操作也会被调用。
6) 异常堆栈跟踪:通过Throwable的getStackTrace方法获取在异常即将被抛出的时间点上程序的调用堆栈,这样有利于日志的输出和错误的分析,见如下代码:
2 try {
3 //TODO: call function, which may be raise some exception.
4 } catch (Throwable e) {
5 StackTraceElement[] frames = e.getStackTrace();
6 for (StackTraceElement f : frames) {
7 System.out.printf("Filename is = %s\n",f.getFileName());
8 System.out.printf("LineNumber is = %d\n",f.getLineNumber());
9 System.out.printf("ClassName is = %s\n",f.getClassName());
10 System.out.printf("Methodname is = %s\n",f.getMethodName());
11 System.out.printf("isNativeMethod = %s\n",f.isNativeMethod() ? "true" : "false");
12 }
13 }
14 }
也可以直接通过Throwable对象函数当前函数的运行栈信息,见如下代码:
2 Throwable e = new Throwable();
3 StackTraceElement[] frames = e.getStackTrace();
4 for (StackTraceElement f : frames) {
5 System.out.printf("Filename is = %s\n",f.getFileName());
6 System.out.printf("LineNumber is = %d\n",f.getLineNumber());
7 System.out.printf("ClassName is = %s\n",f.getClassName());
8 System.out.printf("Methodname is = %s\n",f.getMethodName());
9 System.out.printf("isNativeMethod = %s\n",f.isNativeMethod() ? "true" : "false");
10 }
11 }
12 /* 输入如下:
13 Filename is = TestMain.java
14 LineNumber is = 3
15 ClassName is = TestMain
16 Methodname is = main
17 isNativeMethod = false */
C++语言本身并未提供这样的方法,只是提供了__FUNCTION__、__LINE__、__FILE__这样的3个宏来获取当前函数的函数名、行号和文件名,但是无法得到调用栈信息,如果确实需要这样的信息,只能通过操作系统的工具包来协助完成(仅针对Debug版本),目前Windows(vc)和Linux(gcc)都提供这样的开发包。
2. 断言:是主要用于开发、调试和系统集成测试期间进行Debug的一种方式和技巧,语法如下:
assert condition OR assert condition : expression
其中assert为关键字,当condition为false时,程序运行中断,同时报出指定的错误信息,如果使用assert的后面一种形式,expression的结果将会同时输出,这样更有助于错误的判断,见如下两种代码形式:
2 int a = 5;
3 assert a > 10 : a;
4 System.out.println("Ok.");
5 }
6 /* 输出结果:
7 Exception in thread "main" java.lang.AssertionError: 5
8 at TestMain.main(TestMain.java:4)
9 */
10 public static void main(String[] args) {
11 int[] a = null;
12 assert a != null;
13 System.out.println("Ok.");
14 }
15 /* 输出结果:
16 Exception in thread "main" java.lang.AssertionError
17 at TestMain.main(TestMain.java:4)
18 */
在eclipse中,缺省情况下断言是被禁用的,如果需要开启断言,则需要在Run(Debug) As->Run(Debug) Configurations...->Arguments->VM arguments中添加"-enableassertions" 运行期参数。如果断言被禁用,assert中的代码不会被执行,因此在系统发布后也不会影响程序的运行时效率。使用者也可以通过该命令行参数-ea:MyClass -ea:com.mypackage.mylib 来指定需要启用断言的class和package,如果启用的是package,那么该包内的所有class都将启用断言。在C++中,是依靠crt中的assert(cond)函数来实现的,如果cond为false,程序将会立即停止,但是在使用前首先需要保证assert.h文件被包含进当前文件,再有就是当前编译的程序必须是Debug版本,对于Release版本,无论Win32和Linux,断言的语句都将不会被执行。