ホームページ  >  記事  >  Java  >  Java 例外に関するいくつかのパズルの詳細な分析

Java 例外に関するいくつかのパズルの詳細な分析

黄舟
黄舟オリジナル
2017-06-18 09:46:461218ブラウズ

この記事は、Java の異常に関するいくつかのパズルを集めて整理したもので、非常に優れており、必要な友人は参照できます

パズル 1: 優柔不断

次のプログラムを見てください。結局何?


public class Indecisive {
 public static void main(String[] args) {
  System.out.println(decision());
 }
 private static boolean decision() {
  try {
   return true;
  } finally {
   return false;
  }
 }
}

実行結果:

false

結果の説明:

try-finally ステートメントでは、制御が try ステートメント ブロックから離れると、finally ステートメント ブロックが常に実行されます。これは、try ブロックが正常に終了したか予期せず終了したかに関係なく当てはまります。

ステートメントまたはステートメントのブロックは、例外をスローしたり、閉じられたステートメントで Break または continue を実行したり、このプログラムのようなメソッドで return を実行したりすると、予期せず終了します。これらは、プログラムが次のステートメントを順番に実行するのを妨げるため、予期せぬ終了と呼ばれます。 try ブロックとfinally ブロックの両方が予期せず終了した場合、try ブロック内の予期せぬ終了の理由は破棄され、try-finally ステートメント全体が予期せず終了する理由は予期せぬ終了の理由と同じになります。ファイナルブロックの。このプログラムでは、try ブロック内の return ステートメントによって引き起こされる予期せぬ終了は破棄されます。try-finally ステートメントの予期しない終了は、finally ブロック内の return によって引き起こされます。

簡単に言えば、プログラムはtrueを返そうと(試みて)(返そうと)しますが、最終的にはfalseを返します(返す)。予期せぬ終了の原因を破棄することは、望ましい動作であることはほとんどありません。予期せぬ終了の元の原因の方が、プログラムの動作にとってより重要であるように見える場合があるためです。特に、try ブロック内で Break、Continue、または return ステートメントを実行し、その動作が Final ブロッ​​クによってオーバーライドされるプログラムの動作を理解するのは特に困難です。つまり、すべてのfinallyステートメントブロックは、未チェック例外がスローされない限り、正常に終了する必要があります。 return、break、 continue、または throw を使用して、finally ブロックを決して終了しないでください。また、チェックされた例外が、finally ブロックの外に伝播されることを決して許可しないでください。言語設計者にとっては、おそらく、finally ステートメント ブロックが未チェックの例外なしで正常に終了する必要があることが求められるはずです。この目標に向けて、try-finally 構造では、finally ステートメント ブロックが正常に終了できることが必要になります。 Final ブロッ​​クの外側に制御を移す return、break、または continue ステートメントは、finally ブロックの外側に伝播するチェック例外を発生させるステートメントと同様に禁止する必要があります。

パズル 2: 非常に素晴らしい

以下の 3 つのプログラムはそれぞれ何を出力しますか? それらがすべてコンパイルできるとは考えないでください。

最初のプログラム


import java.io.IOException;
public class Arcane1 {
 public static void main(String[] args) {
  try {
   System.out.println("Hello world");
  } catch(IOException e) {
   System.out.println("I've never seen println fail!");
  }
 }
}

2番目のプログラム


public class Arcane2 {
 public static void main(String[] args) {
  try {
   // If you have nothing nice to say, say nothing
  } catch(Exception e) {
   System.out.println("This can't happen");
  }
 }
}

3番目のプログラム


interface Type1 {
 void f() throws CloneNotSupportedException;
}
interface Type2 {
 void f() throws InterruptedException;
}
interface Type3 extends Type1, Type2 {
}
public class Arcane3 implements Type3 {
 public void f() {
  System.out.println("Hello world");
 }
 public static void main(String[] args) {
  Type3 t3 = new Arcane3();
  t3.f();
 }
}

実行結果:

(01) 最初のプログラムのコンパイルエラー!


Arcane1.java:9: exception java.io.IOException is never thrown in body of corresponding try statement
  } catch(IOException e) {
   ^
 error

(02) 2 番目のプログラムはコンパイルされ、正常に実行されます。

(03) 3 番目のプログラムは正常にコンパイルして実行できます。出力結果は次のとおりです: Hello world

結果の説明:

(01) Arcane1 は、チェック例外の 基本原則 を示します。これはコンパイルできるようです。try 句は I/O を実行し、catch 句は IOException 例外をキャッチします。しかし、println メソッドがチェック例外をスローするように宣言されておらず、IOException がまさにチェック例外であるため、このプログラムはコンパイルできません。言語仕様では次のように説明されています。 catch 節がタイプ E のチェック例外をキャッチし、それに対応する try 節が E のサブタイプの例外をスローできない場合、これはコンパイル時エラーです。

(02) 同じ理由で、2 番目のプログラム Arcane2 はコンパイルできないように見えますが、コンパイルできます。これは、唯一の catch 句が例外をチェックするため、コンパイルされます。この時点では非常に曖昧ですが、対応する try 句の内容に関係なく、Exception または Throwble をキャッチする catch 句は正当です。 Arcane2 は合法的なプログラムですが、catch 句の内容は決して実行されず、プログラムは何も出力しません。

(03) 3 番目のプログラム Arcane3 もコンパイルできないようです。メソッド f は、チェック例外 CloneNotSupportedException をスローするために Type1 インターフェースで宣言され、チェック例外 InterruptedException をスローするために Type2 インターフェースで宣言されます。 Type3 インターフェイスは Type1 と Type2 を継承しているため、静的型 Type3 のオブジェクトに対してメソッド f を呼び出すと、これらの例外がスローされる可能性があるようです。メソッドは、メソッド本体がスローできるすべてのチェック済み例外をキャッチするか、これらの例外をスローすることを宣言する必要があります。 Arcane3 の main メソッドは、静的型 Type3 オブジェクトのメソッド f を呼び出しますが、

CloneNotSupportedException と InterruptedExceptioin を処理しません。では、なぜこのプログラムはコンパイルできるのでしょうか?

  上述分析的缺陷在于对“Type3.f 可以抛出在 Type1.f 上声明的异常和在 Type2.f 上声明的异常”所做的假设。这并不正确,因为每一个接口都限制了方法 f 可以抛出的被检查异常集合。一个方法可以抛出的被检查异常集合是它所适用的所有类型声明要抛出的被检查异常集合的交集,而不是合集。因此,静态类型为 Type3 的对象上的 f 方法根本就不能抛出任何被检查异常。因此,Arcane3可以毫无错误地通过编译,并且打印 Hello world。 

谜题3: 不受欢迎的宾客

下面的程序会打印出什么呢?


public class UnwelcomeGuest {
 public static final long GUEST_USER_ID = -1;
 private static final long USER_ID;
 static {
  try {
   USER_ID = getUserIdFromEnvironment();
  } catch (IdUnavailableException e) {
   USER_ID = GUEST_USER_ID;
   System.out.println("Logging in as guest");
  }
 }
 private static long getUserIdFromEnvironment() 
  throws IdUnavailableException {
  throw new IdUnavailableException();
 }
 public static void main(String[] args) {
  System.out.println("User ID: " + USER_ID);
 }
}
class IdUnavailableException extends Exception {
}

运行结果:


UnwelcomeGuest.java:10: variable USER_ID might already have been assigned
   USER_ID = GUEST_USER_ID;
   ^
 error

结果说明:

  该程序看起来很直观。对 getUserIdFromEnvironment 的调用将抛出一个异常, 从而使程序将 GUEST_USER_ID(-1L)赋值给 USER_ID, 并打印 Loggin in as guest。 然后 main 方法执行,使程序打印 User ID: -1。表象再次欺骗了我们,该程序并不能编译。如果你尝试着去编译它, 你将看到和一条错误信息

  问题出在哪里了?USER_ID 域是一个空 final(blank final),它是一个在声明中没有进行初始化操作的 final 域。很明显,只有在对 USER_ID 赋值失败时,才会在 try 语句块中抛出异常,因此,在 catch 语句块中赋值是相 当安全的。不管怎样执行静态初始化操作语句块,只会对 USER_ID 赋值一次,这正是空 final 所要求的。为什么编译器不知道这些呢? 要确定一个程序是否可以不止一次地对一个空 final 进行赋值是一个很困难的问题。事实上,这是不可能的。这等价于经典的停机问题,它通常被认为是不可能解决的。为了能够编写出一个编译器,语言规范在这一点上采用了保守的方式。在程序中,一个空 final 域只有在它是明确未赋过值的地方才可以被赋值。规范长篇大论,对此术语提供了一个准确的但保守的定义。 因为它是保守的,所以编译器必须拒绝某些可以证明是安全的程序。这个谜题就展示了这样的一个程序。幸运的是, 你不必为了编写 Java 程序而去学习那些骇人的用于明确赋值的细节。通常明确赋值规则不会有任何妨碍。如果碰巧你编写了一个真的可能会对一个空final 赋值超过一次的程序,编译器会帮你指出的。只有在极少的情况下,就像本谜题一样, 你才会编写出一个安全的程序, 但是它并不满足规范的形式化要求。编译器的抱怨就好像是你编写了一个不安全的程序一样,而且你必须修改你的程序以满足它。

  解决这类问题的最好方式就是将这个烦人的域从空 final 类型改变为普通的final 类型,用一个静态域的初始化操作替换掉静态的初始化语句块。实现这一点的最佳方式是重构静态语句块中的代码为一个助手方法:


public class UnwelcomeGuest {
 public static final long GUEST_USER_ID = -1;
 private static final long USER_ID = getUserIdOrGuest();
 private static long getUserIdOrGuest() {
  try {
   return getUserIdFromEnvironment();
  } catch (IdUnavailableException e) {
   System.out.println("Logging in as guest");
   return GUEST_USER_ID;
  }
 }
 private static long getUserIdFromEnvironment() 
  throws IdUnavailableException {
  throw new IdUnavailableException();
 }
 public static void main(String[] args) {
  System.out.println("User ID: " + USER_ID);
 }
}
class IdUnavailableException extends Exception {
}

  程序的这个版本很显然是正确的,而且比最初的版本根据可读性,因为它为了域值的计算而增加了一个描述性的名字, 而最初的版本只有一个匿名的静态初始化操作语句块。将这样的修改作用于程序,它就可以如我们的期望来运行了。总之,大多数程序员都不需要学习明确赋值规则的细节。该规则的作为通常都是正确的。如果你必须重构一个程序,以消除由明确赋值规则所引发的错误,那么你应该考虑添加一个新方法。这样做除了可以解决明确赋值问题,还可以使程序的可读性提高。 

谜题4: 您好,再见!

下面的程序将会打印出什么呢?


public class HelloGoodbye {
 public static void main(String[] args) {
  try {
   System.out.println("Hello world");
   System.exit(0);
  } finally {
   System.out.println("Goodbye world");
  }
 }
}

运行结果:

Hello world

结果说明:

  这个程序包含两个 println 语句: 一个在 try 语句块中, 另一个在相应的 finally语句块中。try 语句块执行它的 println 语句,并且通过调用 System.exit 来提前结束执行。在此时,你可能希望控制权会转交给 finally 语句块。然而,如果你运行该程序,就会发现它永远不会说再见:它只打印了 Hello world。这是否违背了"Indecisive示例" 中所解释的原则呢? 不论 try 语句块的执行是正常地还是意外地结束, finally 语句块确实都会执行。然而在这个程序中,try 语句块根本就没有结束其执行过程。System.exit 方法将停止当前线程和所有其他当场死亡的线程。finally 子句的出现并不能给予线程继续去执行的特殊权限。

  当 System.exit 被调用时,虚拟机在关闭前要执行两项清理工作。首先,它执行所有的关闭挂钩操作,这些挂钩已经注册到了 Runtime.addShutdownHook 上。这对于释放 VM 之外的资源将很有帮助。务必要为那些必须在 VM 退出之前发生的行为关闭挂钩。下面的程序版本示范了这种技术,它可以如我们所期望地打印出 Hello world 和 Goodbye world:


public class HelloGoodbye1 {
 public static void main(String[] args) {
  System.out.println("Hello world");
  Runtime.getRuntime().addShutdownHook(
  new Thread() {
   public void run() {
   System.out.println("Goodbye world");
   }
  });
  System.exit(0);
 }
}

  VM 执行在 System.exit 被调用时执行的第二个清理任务与终结器有关。如果System.runFinalizerOnExit 或它的魔鬼双胞胎 Runtime.runFinalizersOnExit被调用了,那么 VM 将在所有还未终结的对象上面调用终结器。这些方法很久以前就已经过时了,而且其原因也很合理。无论什么原因,永远不要调用System.runFinalizersOnExit 和 Runtime.runFinalizersOnExit: 它们属于 Java类库中最危险的方法之一[ThreadStop]。调用这些方法导致的结果是,终结器会在那些其他线程正在并发操作的对象上面运行, 从而导致不确定的行为或导致死锁。

  总之,System.exit 将立即停止所有的程序线程,它并不会使 finally 语句块得到调用,但是它在停止 VM 之前会执行关闭挂钩操作。当 VM 被关闭时,请使用关闭挂钩来终止外部资源。通过调用 System.halt 可以在不执行关闭挂钩的情况下停止 VM,但是这个方法很少使用。

谜题5: 不情愿的构造器

下面的程序将打印出什么呢?


public class Reluctant {
 private Reluctant internalInstance = new Reluctant();
 public Reluctant() throws Exception {
  throw new Exception("I'm not coming out");
 }
 public static void main(String[] args) {
  try {
   Reluctant b = new Reluctant();
   System.out.println("Surprise!");
  } catch (Exception ex) {
   System.out.println("I told you so");
  }
 }
}

运行结果:


Exception in thread "main" java.lang.StackOverflowError
 at Reluctant.<init>(Reluctant.java:3)
 ...

结果说明:

  main 方法调用了 Reluctant 构造器,它将抛出一个异常。你可能期望 catch 子句能够捕获这个异常,并且打印 I told you so。凑近仔细看看这个程序就会发现,Reluctant 实例还包含第二个内部实例,它的构造器也会抛出一个异常。无论抛出哪一个异常,看起来 main 中的 catch 子句都应该捕获它,因此预测该程序将打印 I told you 应该是一个安全的赌注。但是当你尝试着去运行它时,就会发现它压根没有去做这类的事情:它抛出了 StackOverflowError 异常,为什么呢?

  与大多数抛出 StackOverflowError 异常的程序一样,本程序也包含了一个无限递归。当你调用一个构造器时,实例变量的初始化操作将先于构造器的程序体而运行[JLS 12.5]。在本谜题中, internalInstance 变量的初始化操作递归调用了构造器,而该构造器通过再次调用 Reluctant 构造器而初始化该变量自己的 internalInstance 域,如此无限递归下去。这些递归调用在构造器程序体获得执行机会之前就会抛出 StackOverflowError 异常,因为 StackOverflowError 是 Error 的子类型而不是 Exception 的子类型,所以 catch 子句无法捕获它。对于一个对象包含与它自己类型相同的实例的情况,并不少见。例如,链接列表节点、树节点和图节点都属于这种情况。你必须非常小心地初始化这样的包含实例,以避免 StackOverflowError 异常。

  至于本谜题名义上的题目:声明将抛出异常的构造器,你需要注意,构造器必须声明其实例初始化操作会抛出的所有被检查异常。 

谜题6: 域和流

下面的方法将一个文件拷贝到另一个文件,并且被设计为要关闭它所创建的每一个流,即使它碰到 I/O 错误也要如此。遗憾的是,它并非总是能够做到这一点。为什么不能呢,你如何才能订正它呢?


static void copy(String src, String dest) throws IOException {
 InputStream in = null;
 OutputStream out = null;
 try {
  in = new FileInputStream(src);
  out = new FileOutputStream(dest);
  byte[] buf = new byte[1024];
  int n;
  while ((n = in.read(buf)) > 0)
   out.write(buf, 0, n);
 } finally {
  if (in != null) in.close();
  if (out != null) out.close();
 }
}

谜题分析:

  这个程序看起来已经面面俱到了。其流域(in 和 out)被初始化为 null,并且新的流一旦被创建,它们马上就被设置为这些流域的新值。对于这些域所引用的流,如果不为空,则 finally 语句块会将其关闭。即便在拷贝操作引发了一个 IOException 的情况下,finally 语句块也会在方法返回之前执行。出什么错了呢?

  问题在 finally 语句块自身中。close 方法也可能会抛出 IOException 异常。如果这正好发生在 in.close 被调用之时,那么这个异常就会阻止 out.close 被调用,从而使输出流仍保持在开放状态。请注意,该程序违反了"优柔寡断" 的建议:对 close 的调用可能会导致 finally 语句块意外结束。遗憾的是,编译器并不能帮助你发现此问题,因为 close 方法抛出的异常与 read 和 write 抛出的异常类型相同,而其外围方法(copy)声明将传播该异常。解决方式是将每一个 close 都包装在一个嵌套的 try 语句块中。
下面的 finally 语句块的版本可以保证在两个流上都会调用 close:


try {
 // 和之前一样
} finally {
 if (in != null) {
  try {
   in.close();
  } catch (IOException ex) {
   // There is nothing we can do if close fails
  }
 }
 if (out != null) {
  try {
   out.close();
  } catch (IOException ex) {
   // There is nothing we can do if close fails
  }
 }
}

  总之,当你在 finally 语句块中调用 close 方法时,要用一个嵌套的 try-catch 语句来保护它,以防止 IOException 的传播。更一般地讲,对于任何在 finally 语句块中可能会抛出的被检查异常都要进行处理,而不是任其传播。 

谜题7: 异常为循环而抛

下面的程序会打印出什么呢?


public class Loop {
 public static void main(String[] args) {
  int[][] tests = { { 6, 5, 4, 3, 2, 1 }, { 1, 2 },
   { 1, 2, 3 }, { 1, 2, 3, 4 }, { 1 } };
  int successCount = 0;
  try {
   int i = 0;
   while (true) {
    if (thirdElementIsThree(tests[i++]))
     successCount ++;
   }
  } catch(ArrayIndexOutOfBoundsException e) {
   // No more tests to process
  }
  System.out.println(successCount);
 }
 private static boolean thirdElementIsThree(int[] a) {
  return a.length >= 3 & a[2] == 3;
 }
}

运行结果:

0

结果说明:

  该程序主要说明了两个问题。

  第1个问题:不应该使用异常作为终止循环的手段!

  该程序用 thirdElementIsThree 方法测试了 tests 数组中的每一个元素。遍历这个数组的循环显然是非传统的循环:它不是在循环变量等于数组长度的时候终止,而是在它试图访问一个并不在数组中的元素时终止。尽管它是非传统的,但是这个循环应该可以工作。

  如果传递给 thirdElementIsThree 的参数具有 3 个或更多的元素,并且其第三个元素等于 3,那么该方法将返回 true。对于 tests中的 5 个元素来说,有 2 个将返回 true,因此看起来该程序应该打印 2。如果你运行它,就会发现它打印的时 0。肯定是哪里出了问题,你能确定吗? 事实上,这个程序犯了两个错误。第一个错误是该程序使用了一种可怕的循环惯用法,该惯用法依赖的是对数组的访问会抛出异常。这种惯用法不仅难以阅读, 而且运行速度还非常地慢。不要使用异常来进行循环控制;应该只为异常条件而使用异常。为了纠正这个错误,可以将整个 try-finally 语句块替换为循环遍历数组的标准惯用法:


for (int i = 0; i < test.length; i++)
  if (thirdElementIsThree(tests[i]))
    successCount++;

如果你使用的是 5.0 或者是更新的版本,那么你可以用 for 循环结构来代替:


for (int[] test : tests)
  if(thirdElementIsThree(test))
    successCount++;

  第2个问题: 主要比较"&操作符" 和 "&&操作符"的区别。注意示例中的操作符是&,这是按位进行"与"操作。

以上がJava 例外に関するいくつかのパズルの詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。