>  기사  >  Java  >  Java 예외에 대한 여러 가지 퍼즐에 대한 자세한 분석

Java 예외에 대한 여러 가지 퍼즐에 대한 자세한 분석

黄舟
黄舟원래의
2017-06-18 09:46:461268검색

이 기사는 모든 사람을 위한 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 문이 예기치 않게 종료되는 이유는 예기치 않게 종료되는 이유와 동일합니다. finally 블록의 이 프로그램에서는 try 블록의 return 문으로 인해 발생한 예기치 않은 종료가 삭제됩니다. try-finally 문의 예기치 않은 종료는 finally 블록의 return으로 인해 발생합니다.

  간단히 말해서 프로그램은 true를 반환하기 위해 (시도) (반환)을 시도하지만 결국 false를 (반환)합니다. 예상치 못한 종료의 원인을 버리는 것은 결코 바람직하지 않은 행동입니다. 왜냐하면 예상치 못한 종료의 원래 원인이 프로그램 동작에 더 중요한 것처럼 보일 수 있기 때문입니다. try 블록 내에서 break, continue 또는 return 문을 실행한 후 finally 블록으로 동작을 재정의하는 프로그램의 동작을 이해하는 것은 특히 어렵습니다. 즉, 확인되지 않은 예외가 발생하지 않는 한 모든 finally 문 블록은 정상적으로 종료되어야 합니다. return, break, continue 또는 throw를 사용하여 finally 블록을 종료하지 말고, 확인된 예외가 finally 블록 외부로 전파되는 것을 허용하지 마세요. 언어 디자이너의 경우 finally 문 블록이 확인되지 않은 예외 없이 정상적으로 종료되어야 하는 것이 필요할 수 있습니다. 이 목표를 위해 try-finally 구성에서는 finally 문 블록이 정상적으로 끝날 수 있어야 합니다. finally 블록 외부로 제어를 전달하는 Return, break 또는 continue 문은 금지되어야 하며, finally 블록 외부로 전파되는 확인된 예외를 발생시키는 모든 문도 마찬가지입니다.

퍼즐 2: 정말 놀랍습니다.

아래 세 가지 프로그램은 각각 무엇을 인쇄할까요? 모두 컴파일할 수 있다고 가정하지 마세요.

첫 번째 프로그램


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!");
  }
 }
}

두 번째 프로그램


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");
  }
 }
}

세 번째 프로그램


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) 두 번째 프로그램은 정상적으로 컴파일되고 실행됩니다.

(03) 세 번째 프로그램은 정상적으로 컴파일되고 실행될 수 있습니다. 출력 결과는 다음과 같습니다: Hello world

결과 설명:

(01) Arcane1은 확인된 예외의 기본 원칙을 보여줍니다. 컴파일해야 하는 것처럼 보입니다. try 절은 I/O를 수행하고 catch 절은 IOException 예외를 포착합니다. 그러나 이 프로그램은 println 메소드가 체크된 예외를 발생시키도록 선언되지 않았고 IOException은 정확히 체크된 예외이기 때문에 컴파일될 수 없습니다. 언어 사양에서는 다음과 같이 설명합니다. catch 절이 E 유형의 확인된 예외를 포착하고 해당 try 절이 E 하위 유형의 예외를 throw할 수 없는 경우 이는 컴파일 시간 오류입니다.

(02) 같은 이유로 두 번째 프로그램인 Arcane2는 컴파일이 안 될 것 같지만 컴파일이 됩니다. 유일한 catch 절이 예외를 확인하기 때문에 컴파일됩니다. 현 시점에서는 매우 모호하지만 Exception이나 Throwble을 잡는 catch 절은 해당 try 절의 내용에 관계없이 적법합니다. Arcane2는 합법적인 프로그램이지만 catch 절의 내용은 절대 실행되지 않으며 프로그램은 아무것도 인쇄하지 않습니다.

(03) 세 번째 프로그램인 Arcane3도 컴파일되지 않는 것 같습니다. 메소드 f는 확인된 예외 CloneNotSupportedException을 발생시키기 위해 Type1 인터페이스에서 선언되고, 확인된 예외 InterruptedException을 발생시키기 위해 Type2 인터페이스에서 선언됩니다. Type3 인터페이스는 Type1 및 Type2에서 상속되므로 정적 유형 Type3의 개체에 대해 f 메서드를 호출하면 잠재적으로 이러한 예외가 발생하는 것으로 보입니다. 메서드는 해당 메서드 본문에서 발생할 수 있는 모든 확인된 예외를 포착하거나 이러한 예외를 발생시키겠다고 선언해야 합니다. Arcane3의 기본 메서드는 정적 유형 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.