yeskery

异常捕捉的陷阱

异常处理机制是 Java 语言的特色之一,尤其是 Java 语言的 Checked 异常,更是体现了 Java 语言的严谨性:没有完善错误处理的代码根本就不会被执行。对于 Checked 异常,Java 程序要么声明抛出,要么使用 try……catch 进行捕捉。

每个进行 Java 开发的开发者都无法回避异常处理,而 Java 的异常处理同样也存在一些容易让人迷糊的地方。例如,finally 块的执行规则到底是怎样的?程序遇到 return 语句后是否还会执行 finally 块?程序遇到 System.exit() 语句后是否还会执行 finally 块?除此之外,使用 catch 块捕捉异常不当时也可能导致程序错误,这些问题都值得每个开发者认真对待。

正确关闭资源的方式

在实际开发中,经常需要在程序中打开一些物理资源,如数据库连接、网络连接、磁盘文件等,打开这些物理资源之后必须显式关闭,否则将会引起资源泄漏。

可能有读者为觉得,JVM 不是提供了垃圾回收机制吗?JVM 的垃圾回收机制难道不会回收这些资源吗?答案是不会。前面介绍过,垃圾回收机制属于 Java 内存管理的一部分,它只是负责回收堆内存中分配出来的内存,至于程序中打开的物理资源,垃圾回收机制是无能为力的。

为了正常关闭程序中打开的物理资源,应该使用 finally 块来保证回收。下面程序示范了先通过序列化机制将 Java 对象写入磁盘,然后再通过反序列化机制恢复 Java 对象,程序应该正常关闭对象输入流和对象输出流。

  1. class Wolf implements Serializable {
  2. private String name;
  3. public Wolf(String name) {
  4. System.out.println("调用有参数的构造器");
  5. this.name = name;
  6. }
  7. @Override
  8. public int hashCode() {
  9. return name.hashCode();
  10. }
  11. @Override
  12. public boolean equals(Object obj) {
  13. if (obj == this) {
  14. return true;
  15. }
  16. if (obj.getClass() == Wolf.class) {
  17. Wolf target = (Wolf)obj;
  18. return target.name.equals(this.name);
  19. }
  20. return false;
  21. }
  22. }
  23. public class CloseResource {
  24. public static void main(String[] args) throws Exception {
  25. Wolf w = new Wolf("灰太狼");
  26. System.out.println("Wolf对象创建完成");
  27. Wolf w2 = null;
  28. ObjectOutputStream oos = null;
  29. ObjectInputStream ois = null;
  30. try {
  31. //创建对象输出流
  32. oos = new ObjectOutputStream(new FileOutputStream("a.bin"));
  33. //创建对象输入流
  34. ois = new ObjectInputStream(new FileInputStream("a.bin"));
  35. //序列化输出Java对象
  36. oos.writeObject(w);
  37. oos.flush();
  38. //反序列化恢复Java对象
  39. w2 = (Wolf)ois.readObject();
  40. //使用finally块来回收资源
  41. } finally {
  42. oos.close();
  43. ois.close();
  44. }
  45. }
  46. }

正如以上代码所示,程序已经使用 finally 块来保证资源被关闭。那么,这个程序的关闭是否安全呢?答案是否定的。因为程序开始时指定 oos = null;ois = null,完全有可能在程序运行过程中初始化 oos 之前就引发了异常,那么 oos、ois 还未来得及初始化,因此 oos、ois 根本无需关闭。

为了改变这种直接关闭 oos、ois 的代码,将 finally 块中的代码修改为如下所示:

  1. } finally {
  2. if (oos != null) {
  3. oos.close();
  4. }
  5. if (ois != null) {
  6. ois.close();
  7. }
  8. }

对上面程序进行一些改进,首先保证 oos 不为 null 才关闭,再保证 ois 不为 null 才关闭。

这样看起来够安全了吧,实际上依然不够安全。假如程序开始已经正常初始化了 oos、ois 两个 IO 流,在关闭 oos 时出现了异常,那么程序将在关闭 oos 时非正常退出,这样就会导致 ois 得不到关闭,从而导致资源泄漏。

为了保证关闭各资源时出现的异常不会相互影响,应该在关闭每个资源时分开使用 try……catch 块来保证关闭操作不会导致非正常退出。也就是将 finally 块中的代码修改为如下所示:

  1. } finally {
  2. if (oos != null) {
  3. try {
  4. oos.close();
  5. } catch (Exception e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. if (ois != null) {
  10. try {
  11. ois.close();
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  17. }

上面程序所示的资源关闭方式才是比较安全的,这种关闭方式主要保证如下 3 点:

  • 使用 finally 块来关闭物理资源,保证关闭操作总是被执行;
  • 关闭每个资源之前首先保证引用该资源的引用变量不为 null;
  • 为每个物理资源使用单独的 try……catch 块关闭资源,保证关闭资源时引发的异常不会影响其它资源的关闭。

在后面的代码中为了节省篇幅,都没有严格使用上面的这种方式来关闭资源。

finally 块的陷阱

前面介绍说,finally 块代表总是会被执行的代码块,因此通常总是使用 finally 块来关闭物理资源,从而保证程序物理资源总能被正常关闭。

finally 的执行规则

前面介绍说,finally 块代表总是会被执行的代码块,但有一种情况例外。下面程序尝试打开了一个磁盘输出流,然后 finally 块来关闭这个磁盘输出流。

  1. public class ExitFinally {
  2. public static void main(String[] args) throws Exception {
  3. FileOutputStream fos = null;
  4. try {
  5. fos = new FileOutputStream("a.bin");
  6. System.out.println("程序打开物理资源!");
  7. System.exit(0);
  8. } finally {
  9. //使用finally块关闭资源
  10. if (fos != null) {
  11. try {
  12. fos.close();
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. System.out.println("程序关闭了物理资源!");
  18. }
  19. }
  20. }

这个程序与前面程序略有不同的是:程序的 try 块中增加了 System.exit(0) 来退出程序。如果程序在执行 System.exit(0) 后,finally 块是否还会得到执行的机会?尝试运行上面程序,看到程序有时并不会执行 finally 块的代码。

不论 try 块是正常结束,还是中途非正常地退出,finally 块确实都会执行。然而在这个程序中,try 语句块根本就没有结束其执行过程,System.exit(0) 将停止当前线程和所有其它当场死亡的线程。finally 块并不能让已经停止的线程继续执行。

System.exit(0) 被调用时,虚拟机退出前要执行两项清理工作:

  • 执行系统中注册的所有关闭钩子;
  • 如果程序调用了 System.runFinalizerOnExit(true),那么JVM 会对所有还未结束的对象调用 Finalizer。

第二种方式已经被证实是极度危险的,因此 JDK API 文档中说明第二个方式已经过时了,因此实际开发中不应该使用这种危险行为。

第一种方式则是一种安全的操作,程序可以将关闭资源的操作注册称为关闭钩子。在 JVM 退出之前,这些关闭钩子将会被调用,从而保证物理资源被正常关闭。可以将上面程序改为如下形式:

  1. public class ExitHook {
  2. public static void main(String[] args) throws IOException {
  3. final FileOutputStream fos;
  4. fos = new FileOutputStream("a.bin");
  5. System.out.println("程序打开物理资源!");
  6. //为系统注册关闭钩子
  7. Runtime.getRuntime().addShutdownHook(new Thread(){
  8. @Override
  9. public void run() {
  10. //使用关闭钩子来关闭资源
  11. if (fos != null) {
  12. try {
  13. fos.close();
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. System.out.println("程序关闭了物理资源!");
  19. }
  20. });
  21. //退出程序
  22. System.exit(0);
  23. }
  24. }

上面的程序中为系统注册了一个关闭钩子,关闭钩子负责在程序退出时回收系统资源。运行上面的程序,看到系统可以正常关闭物理资源。

finally 块和方法返回值

通过上面介绍可以看出,只要 Java 虚拟机不退出,不管 try 块正常结束,还是遇到异常非正常退出,finally 块总会获得执行的机会。示例如下:

  1. public class FinallyFlowTest {
  2. public static void main(String[] args) {
  3. int a = test();
  4. System.out.println(a);
  5. }
  6. public static int test() {
  7. int count = 5;
  8. try {
  9. //因为finally块中包含了return语句
  10. //则下面的return语句不会立即返回
  11. return ++count;
  12. } finally {
  13. System.out.println("finally 块被执行");
  14. return count++;
  15. }
  16. }
  17. }

上面程序的 try 块里是 return ++count,而 finally 块则是 return count++,那么这个 test() 方法的返回值到底是多少呢?

运行上面程序,可以发现程序输出 6,而且这表明 test() 方法的返回值是 6,程序也输出了“finally 块被执行”字符串,这表明 finally 块被执行了,也就是说 return count++ 也被执行了。但 test() 方法返回值是 6,表明该代码立即返回了,没有退回 try 块中再次执行 return ++count

当 Java 程序执行 try 块、catch 块时遇到 return 语句,return 语句会导致该方法立即结束。系统执行完 return 语句之后,并不会理解结束该方法,而是去寻找该异常处理流程中是否包含 finally 块,如果没有 finally 块,方法终止,,返回相应的返回值。如果有 finally 块,系统立即开始执行 finally 块————只有当 finally 块执行完成后,系统才会再次跳回来根据 return 语句结束方法。如果 finally 块使用了 return 语句来导致方法结束,则 finally 块已经结束了方法,系统将不会跳回去执行 try 块、catch 块里的任何代码。

下面还有一个比较难以判断的程序:

  1. public class FinallyFlowTest2 {
  2. public static void main(String[] args) {
  3. int a= test();
  4. System.out.println(a);
  5. }
  6. public static int test() {
  7. int count = 5;
  8. try {
  9. throw new RuntimeException("测试异常");
  10. } finally {
  11. System.out.println("finally 块被执行");
  12. return count;
  13. }
  14. }
  15. }

上面程序的 try 块中抛出了 RuntimeException 异常,程序并未使用 catch 块来捕获这个异常。正常情况下,这个异常应该导致 test() 方法非正常终止,test() 方法应该没有返回值。

运行上面的程序,可以发现 test() 方法完全可以正常结束,而且 test() 方法返回了 5,看起来程序中的 throw 语句完全失去了作用。

这也是符合 finally 块执行流程的,当程序执行 try 块、catch 块时遇到 throw 语句时,throw 语句会导致该方法立即结束,系统执行 throw 语句时并不会立即抛出异常,而是去寻找该异常处理流程中是否包含 finally 块。如果没有 finally 块,程序立即抛出异常;如果有 finally 块,系统立即开始执行 finally 块————只有当 finally 块执行完成后,系统才会再次跳回来抛出异常。如果 finally 块使用 return 语句来结束方法,系统将不会跳回去执行 try 块、catch 块去抛出异常。

catch 块的用法

对于 Java 的异常捕捉来说,每个 try 块至少需要一个 catch 块或一个 finally 块,绝不能只有单独一个孤零零的 try 块。一个 try 不仅可以对应一个 catch 块,还可以对应多个 catch 块。

catch 块的顺序

当 Java 运行时环境收到异常对象时,系统会根据 catch(XxxException e) 语句决定使用哪个异常分支来处理程序引发的异常。

当程序进入负责异常处理的 catch 块时,系统生成的异常对象 e 将会被传给 catch(XxxException) 语句的异常形参,不同的 catch 块通过该对象来访问异常的详细信息。

每个 try 块后可以有多个 catch 块,不同的 catch 块针对不同异常类提供相应的异常处理方式。当发生不同意外情况时,系统会生成不同的异常对象,这样可保证 Java 程序能根据该异常对象所属的异常类来决定使用哪个 catch 块处理该异常。

通过 try 块后提供多个 catch 块,可以无需在异常处理块中使用 if、switch 判断异常类型,但依然可以针对不同异常类型提供相应的处理逻辑,从而提供更细致、更有条理的异常处理逻辑。

通常情况下,如果 try 块被执行一次,则 try 块后只有一个 catch 块会被执行,绝不可能有多个 catch 块被执行。除非在循环中使用了 continue 开始下一次循环,下一次循环又重新运行了 try 块,才可能导致多个 catch 块被执行。

由于异常处理机制中排在前面的 catch(XxxException) 块总是会优先获得执行的机会,因此 Java 对 try 块后的多个 catch 块的排列顺序是有要求的。示例如下:

  1. public class CatchSequenceTest {
  2. public static void main(String[] args) throws Exception {
  3. FileInputStream fis = null;
  4. try {
  5. fis = new FileInputStream("a.bin");
  6. fis.read();
  7. //捕捉IOException异常
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. //捕捉FileNotFoundException异常
  11. } catch (FileNotFoundException e) {
  12. e.printStackTrace();
  13. } finally {
  14. //简单方式关闭资源
  15. if (fis != null) {
  16. fis.close();
  17. }
  18. }
  19. }
  20. }

尝试编译上面这个程序,将看到“已捕捉异常 java.io.FileNotFoundException”的编译错误。

因为 Java 的异常有非常严格的继承体系,许多异常之间有严格的父子关系,比如,程序 FileNotFoundException 异常就是 IoException 的子类。根据 Java 继承的特性,子类其实是一种特殊的父类,也就是说,FileNotFoundException 只是一种特殊的 IOException。程序前面的 catch 块已经捕捉了 IOException,这意味着 FileNotFoundException 作为子类已经被捕捉过了,因此程序在后面再次试图捕捉 FileNotFoundException 纯属多此一举。

经过上面分析可以看出,在 try 块后使用 catch 块来捕捉多个异常时,程序应该小心多个 catch 块之前的顺序:捕捉父类异常的 catch 块都应该排在捕捉子类异常的 catch 块之后(简称为,先处理小异常,再处理大异常),否则将出现编译错误。

上面这条规则和前面介绍的 if……else 分支语句的处理规则基本相似:if……else 分支语句应该先处理范围小的条件,后处理范围大的条件,否则将会导致 if……else 分支语句后面的分支得不到执行的机会。try……catch 语句的处理规则也是如此:try……catch 语句的多个 catch 块应该先捕获子类异常(子类代表的范围较小),后捕获父类异常(父类代表的范围较大),否则编译器会提示编译错误。

由于 Exception 是所有异常类的根父类,因此 try……catch 块应该吧捕捉 Exception 的 catch 块排在所有 catch 块的最后面。否则,Java 运行时将直接进入捕捉 Exception 的 catch 块(因为所有异常对象都是 Exception 或其子类的实例),而排在它后面的 catch 块将永远也不会获得执行的机会。当然,编译器还是比较智能的,当检测到程序员试图做这样一件“蠢事”时,编译器会直接提示编译错误,阻止这样的代码获得执行。

进行异常捕获时,一定要记住先捕获小的异常,再捕获大的异常。

不要用 catch 代替流程控制

  1. public class ExceptionFlowTest {
  2. public static void main(String[] args) {
  3. String[] books = {"疯狂Java讲义","轻量级Java EE企业引用实战","疯狂Ajax讲义"};
  4. int i = 0;
  5. while (true) {
  6. try {
  7. System.out.println(books[i++]);
  8. } catch (IndexOutOfBoundsException e) {
  9. //结束循环
  10. break;
  11. }
  12. }
  13. }
  14. }

程序使用“死循环”来遍历数组,当遍历数组产生“数组越界”异常时,会自动捕捉该异常,并跳出“死循环”。程序看上去没有任何问题,尝试运行该程序,确实可以完全遍历该字符串数组里的每个字符串。

看来这个“别出心裁”的主意还不错,完全可以使用这种方式来遍历数组。问题是,这种做法真的好吗?实际上,这种遍历数组的方式不仅难以阅读,而且运行速度还非常慢。

切记:千万不要使用异常来进行流程控制。异常机制不是为流程控制而准备的,异常机制只是为程序的意外情况准备的,因此程序只应该为异常情况使用异常机制。所以,不要使用这种“别出心裁”的方法来遍历数组。

不要在程序中过度使用异常机制,千万不要使用异常处理机制来代替流程控制。对于程序中各种能够预知的情况,应该尽量进行处理,不要盲目地使用异常捕捉来代替流程控制。

只能 catch 可能抛出的异常

在前面大部分程序中,程序通常直接调用 catch(Exception e) 来捕捉所有异常,这个 catch 块可以捕捉所有的程序异常。

  1. public class CatchTest {
  2. public static void main(String[] args) {
  3. test1();
  4. test2();
  5. test3();
  6. test4();
  7. test5();
  8. }
  9. public static void test1() {
  10. try {
  11. System.out.println("hello world!");
  12. } catch (IndexOutOfBoundsException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. public static void test2() {
  17. try {
  18. System.out.println("hello world!");
  19. } catch (NullPointerException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. public static void test3() {
  24. try {
  25. System.out.println("hello world!");
  26. } catch (IOException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. public static void test4() {
  31. try {
  32. System.out.println("hello world!");
  33. } catch (ClassNotFoundException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. public static void test5() {
  38. try {
  39. System.out.println("hello world!");
  40. } catch (Exception e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. }

上面程序中定义了 5 个简单的方法。这 5 个方法所包含的代码非常简单,它们只有一行简单的输出语句,程序试图捕捉这条输出语句可能引发的 5 种异常,如下所示:

  • IndexOutBoundsException
  • NullPointerException
  • IOException
  • ClassNotFoundException
  • Exception

编译上面的程序,将会发现编译器提示“在相应的 try 语句主体中不能抛出 java.io.IOException”以及“在相应的 try 语句主体中不能抛出 java.lang.ClassNotFoundException”编译错误。

编译器认为 System.out.println("hello world!"); 不可能抛出 IOException 和 ClassNotFoundException 这两个异常,因此试图捕捉这两个异常是有错的。

但上面程序中,test1()、test2() 两个方法试图捕捉 IndexOutOfBoundsException、NullPointerException 异常却没有任何错误。很明显,IndexOutOfBoundsException、NullPointerException 这两个异常和 IOException、ClassNotFoundException 这两个异常存在区别。

实际情况也是如此,IndexOutOfBoundsException、NullPointerException 两个异常类都是 RuntimeException 的子类,因此它们都属于运行时异常,而 IOException、ClassNotFoundException 异常则属于 Checked 异常。

根据 Java 语言规范,如果一个 catch 子句试图捕获一个类型为 XxxException 的 Checked 异常,那么它对应的 try 子句必须可能抛出 XxxException 或其子类的异常,否则编译器将提示该程序具有编译错误————但在所有 Checked 异常中,Exception 是一个异类,无论 try 块是怎样的代码,catch(Exception e) 总是正确的。

RumtileException类及其子类的实例被称为 Runtime 异常,不是 RuntimeException 类及其子类的异常实例则被称为 Checked 异常,只要愿意,程序总可以使用 catch(XxxException e) 来捕捉运行时异常。

RumtimeException异常是一种非常灵活的异常,它无需显式声明抛出,只要程序有需要,既可以在任何有需要的地方使用 try……catch 块来捕捉 Rumtime 异常。

前面提到,如果一个 catch 子句试图捕获一个类型为 XxxException 的 Checked 异常,那么它对应的 try 子句则必须可能抛出 XxxException 或其子类的异常。这里有一个问题,如何确定 try 子句可能抛出哪些异常呢?

  1. public class CatchTest2 {
  2. public static void main(String[] args) {
  3. test1();
  4. test2();
  5. }
  6. public static void test1() {
  7. FileInputStream fis = null;
  8. try {
  9. fis = new FileInputStream("a.bin");
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. } finally {
  13. if (fis != null) {
  14. try {
  15. fis.close();
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  21. }
  22. public static void test2() {
  23. try {
  24. Class.forName("com.yeskery.learning.Student");
  25. System.out.println("hello world");
  26. } catch (ClassNotFoundException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. }

上面程序中,test1() 方法也尝试捕捉 IOException 异常,test2() 方法也尝试捕捉 ClassNotFoundException,但程序并未产生任何问题。这就是因为,test1() 方法中 try 块里的代码可能抛出 IOException 异常,test2() 方法中 try 块里的代码可能抛出 ClassNotFoundException 异常。

为什么 test1() 方法的 try 可能抛出 IOException?该 try 块中有如下一行代码:
fis = new FileInputStream("a.bin");
这行代码调用了 FileInputStream 类的构造器来差un关键一个文件输入刘。该构造器声明如下:
public FileInputStream(String name) throws FileNotFoundException
从上面代码方法声明可以看出:FileInputStream 类的构造器声明抛出了一个 FileNotFoundException 异常(它是 IOException 的子类),也就是程序调用该构造器创建输入流可能抛出该异常。在这样的情况下,test1() 方法中的 try 块对应的 catch 块才可以试图捕捉 IOException 异常。

实际上,如果一个代码可能抛出某个 Checked 异常(这段代码调用的某个方法、构造器声明抛出了该 Checked 异常),那么程序必须处理这个 Checked 异常。对于 Checked 异常的处理方式有两种:

  • 当前方法明确知道如何处理该异常,程序应该使用 try……catch 块来捕获该异常,然后在对应的 catch 块中修复该异常;
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

总之,程序使用 catch 捕捉异常时,其实不能随心所欲地捕捉所有异常。程序可以在任意想捕捉的地方捕捉 RuntimeException 异常、Exception,但对于其它 Checked 异常,只有当 try 块可能抛出该异常时(try 块中调用的某个方法声明抛出了该 Checked 异常),catch 块才能捕捉该 Checked 异常。

实际的修复

对于前面介绍的绝大部分程序,程序的 catch 块里并未提供太大有效的修复操作,catch(XxxException) 块内只有一行简单的 e.printStackTrace(); 代码。其实这行代码并没有太多的存在价值,只是打印了异常的跟踪栈信息而已。

实际上,即使程序不捕获该异常,不适用 e.printStackTrace(); 输出异常跟踪栈信息,JVM 遇到异常时也会自动中止程序,并打印异常的跟踪栈信息。

如果程序知道如何修复指定异常,应该在 catch 块内尽量修复该异常,当该异常情况被修复后,可以再次调用该方法;如果程序不知道如何修复该异常,也没有进行任何修复,千万不要再次调用可能导致该异常的方法。示例如下:

  1. public class DoFixThing {
  2. public static void main(String[] args) {
  3. test();
  4. }
  5. public static void test() {
  6. try {
  7. //加载一个类
  8. Class.forName("com.yeskery.learning.Student");
  9. System.out.println("hello world");
  10. } catch (ClassNotFoundException e) {
  11. //不作任何修复,试图再次调用test()方法
  12. test();
  13. }
  14. }
  15. }

上面程序的 test() 方法使用 try……catch 执行 Class.forName("com.yeskery.learning.Student"); 代码,这样代码可能引发导致 ClassNotFoundException 异常。当程序捕捉到该异常时,并未进行任何修复操作,只是简单地试图再次调用 test() 方法。这样做的后果很严重:程序再次调用 test() 方法时将再次引发 ClassNotFoundException 异常,该异常将再次被对应的 catch 块捕捉到……这样就形成了无限递归,程序最终将因为 java.lang.StackOverflowError 错误而非正常结束。

在最坏的情况下,程序使用 finally 块再次调用可能引发异常的方法,这也会导致程序一直进行无限递归,甚至不能因为 StackOverflowError 错误而中止————因为无论是正常结束,还是非正常中止,程序都会执行 finally 块的代码。

  1. public class DoFixThing2 {
  2. public static void main(String[] args) throws Exception {
  3. test();
  4. }
  5. public static void test() throws ClassNotFoundException {
  6. try {
  7. //加载一个类
  8. Class.forName("com.yeskery.learning.Student");
  9. System.out.println("hello world");
  10. } finally {
  11. test();
  12. }
  13. }
  14. }

上面的程序更极端,它不可能抛出异常。因为根据 finally 的执行流程,每当程序试图调用 throw 语句抛出异常时,程序总会先执行 finally 块中代码,这就导致程序进行无限递归,即使程序实际已经发生了 StackOverflowError 错误,依然不会非正常退出。

运行上面程序,将看到程序直接进入“挂起”状态,程序永远不会结束,甚至会导致整个操作系统的运行都非常缓慢,只有通过强制结束 java.exe 进程来结束该程序。

这个程序给出的教训是:无论如何不要在 finally 块中递归调用可能引起异常的方法,因为这将导致该方法的异常不能被正常抛出,甚至 StackOverflowError 错误也不能中止程序,只能通过强行结束 java.exe 进程的方式来中止程序的运行。

继承得到的异常

Java 语言规定,子类重写父类方法时,不能声明抛出比父类方法类型更多、范围更大的异常。也就是说,子类重写父类方法时,子类方法只能声明抛出父类方法所声明抛出的异常的子类。

掌握这个规则后,看下面一个简单的程序:

  1. //定义第一个接口
  2. interface Type1 {
  3. void test() throws ClassNotFoundException;
  4. }
  5. //定义第二个接口
  6. interface Type2 {
  7. void test() throws NoSuchMethodException;
  8. }
  9. //该Test类实现Type1、Type2两个接口
  10. public class Test implements Type1,Type2 {
  11. //实现Type1、Type接口声明的抽象方法
  12. @Override
  13. public void test() throws ClassNotFoundException, NoSuchMethodException {
  14. System.out.println("Hello world");
  15. }
  16. public static void main(String[] args) {
  17. Test t = new Test();
  18. t.test();
  19. }
  20. }

上面程序中定义了 Type1、Type2 连个接口,这两个接口里都定义了一个 test() 方法,只是它们抛出的异常不同而已。接着,程序定义了 Test 类,该 Test 类实现了 Type1、Type2 两个接口,因此 Test 类应该实现这两个接口里的 test() 方法。Test 类实现 test() 方法时声明抛出了 Type1、Type2 两个接口里 test() 方法里声明抛出的异常。

尝试编译上面程序,可以看到编译器提示“Test 中的 test() 无法实现 Type1 中的 test();被覆盖的方法不抛出 java.lang.NoSuchMehodException” 编译错。

删除 test() 方法声明抛出的 NoSuchMethodException 异常,再次尝试编译改程序,看到编译器提示“Test 中的 test() 无法实现 Type2 中的 test();被覆盖的方法不抛出 java.lang.ClassNotFoundException” 编译错误。

只有当删除 Test 类 test() 方法声明抛出的所有异常之后,再次编译该程序,才看到编译完成。通过上面介绍可以发现:Test 类实现了 Type1 接口,实现 Type1 接口里的 test() 方法时可以声明抛出 ClassNotFoundException 异常或该异常的子类,或者不声明抛出;Test 类实现了 Type2 接口,实现 Type2 接口里的 test() 方法时可以抛出 NoSuchMethodException 异常或该异常的子类,或者不声明抛出。由于 Test 类同时实现了 Type1、Type2 两个接口,因此需要同时实现两个接口中的 test() 方法。只能是上面两种声明抛出的交集,不能声明抛出异常。

也就是将上面 Test 类改为如下形式,Test 类才可以正常通过编译。

  1. //该Test类实现Type1、Type2两个接口
  2. public class Test implements Type1,Type2 {
  3. //实现Type1、Type接口声明的抽象方法
  4. @Override
  5. public void test(){
  6. System.out.println("Hello world");
  7. }
  8. public static void main(String[] args) {
  9. Test t = new Test();
  10. t.test();
  11. }
  12. }

本文转载自:《疯狂Java 突破程序员基本功的16课》第八章 异常捕捉的陷阱

评论

发表评论 点击刷新验证码

提示

该功能暂未开放