Maison  >  Article  >  Java  >  Analyse détaillée de plusieurs énigmes sur les exceptions Java

Analyse détaillée de plusieurs énigmes sur les exceptions Java

黄舟
黄舟original
2017-06-18 09:46:461219parcourir

Cet article rassemble et organise plusieurs énigmes sur les anomalies Java pour tout le monde. Il est très bien et a une valeur de référence. Les amis qui en ont besoin peuvent s'y référer

Puzzle 1 : Indécis. 🎜>

Regardez le programme ci-dessous. Qu'est-ce qu'il imprime ?


public class Indecisive {
 public static void main(String[] args) {
  System.out.println(decision());
 }
 private static boolean decision() {
  try {
   return true;
  } finally {
   return false;
  }
 }
}
Résultat de l'exécution :

faux

Description du résultat :

Dans une instruction try-finally Le , enfin, le bloc d'instructions est toujours exécuté lorsque le contrôle quitte le bloc d'instructions try. Cela est vrai que le bloc try se termine normalement ou de manière inattendue.


Une instruction ou un bloc d'instructions lève une exception, exécute un break ou continue sur une instruction fermée, ou exécute un return dans une méthode comme ce programme, une fin inattendue se produira. On les appelle des terminaisons inattendues car elles empêchent le programme d'exécuter les instructions suivantes dans l'ordre. Lorsque le bloc try et le bloc enfin se terminent de manière inattendue, la raison de la fin inattendue dans le bloc try sera ignorée et la raison de la fin inattendue de l'ensemble de l'instruction try-finally sera la même que la raison de l'inattendu. fin du bloc final. Dans ce programme, la fin inattendue provoquée par l'instruction return dans le bloc try sera ignorée. La fin inattendue de l'instruction try-finally est provoquée par le retour dans le bloc enfin.


En termes simples, le programme essaie (essaye) (retourne) de renvoyer vrai, mais il finalement (renvoie) faux. Ignorer la cause d'une fin inattendue n'est presque jamais un comportement souhaitable, car la cause initiale de la fin inattendue peut sembler plus importante pour le comportement du programme. Il est particulièrement difficile de comprendre le comportement des programmes qui exécutent une instruction break, continue ou return dans un bloc try pour ensuite voir leur comportement remplacé par un bloc final. En bref, chaque bloc d'instructions final devrait se terminer normalement à moins qu'une exception non vérifiée ne soit levée. Ne quittez jamais un bloc final avec un return, break, continue ou throw, et ne permettez jamais à une exception vérifiée de se propager en dehors d'un bloc final. Pour les concepteurs de langage, il faudrait peut-être exiger que le bloc d'instructions final se termine correctement sans exception non vérifiée. Pour atteindre cet objectif, la structure try-finally nécessitera que le bloc d'instructions final puisse se terminer normalement. Les instructions return, break ou continue qui transfèrent le contrôle en dehors d'un bloc final doivent être interdites, tout comme toute instruction qui déclenche une exception vérifiée qui se propage en dehors d'un bloc final.


Puzzle 2 : Extrêmement incroyable

Qu'imprime chacun des trois programmes ci-dessous ? Ne présumez pas qu'ils font tous. Par compilation.

Premier programme


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!");
  }
 }
}
Deuxième programme


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");
  }
 }
}
Le troisième programme


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();
 }
}
résultats :

(01) Le premier programme n'a pas été compilé correctement !


Arcane1.java:9: exception java.io.IOException is never thrown in body of corresponding try statement
  } catch(IOException e) {
   ^
 error
(02) Le deuxième programme peut se compiler et s'exécuter normalement.

(03) Le troisième programme peut se compiler et s'exécuter normalement. Le résultat de sortie est : Bonjour tout le monde

Description du résultat :

(01) Arcane1 démontre un

principe de base des exceptions vérifiées. Il semble qu'il devrait compiler : la clause try effectue des E/S et la clause catch intercepte les exceptions IOException. Mais ce programme ne peut pas être compilé car la méthode println n'est pas déclarée pour lever une exception vérifiée, et IOException est exactement une exception vérifiée. La spécification du langage décrit : Si une clause catch intercepte une exception vérifiée de type E et que sa clause try correspondante ne peut pas lever une exception d'un sous-type de E, alors il s'agit d'une erreur de compilation.

(02) Pour la même raison, le deuxième programme, Arcane2, semble ne pas devoir compiler, mais c'est le cas. Il compile parce que sa seule clause catch vérifie l'exception. Bien que cela soit très ambigu à ce stade, une clause catch qui intercepte une Exception ou un Throwble est légale quel que soit le contenu de la clause try correspondante. Bien qu'Arcane2 soit un programme légal, le contenu de la clause catch n'est jamais exécuté et le programme n'imprime rien.

(03) Le troisième programme, Arcane3, semble ne pas se compiler non plus. La méthode f est déclarée dans l'interface Type1 pour lever une exception vérifiée CloneNotSupportedException, et est déclarée dans l'interface Type2 pour lever une exception vérifiée InterruptedException. L'interface Type3 hérite de Type1 et Type2, il semble donc que l'appel de la méthode f sur un objet de type statique Type3 lève potentiellement ces exceptions. Une méthode doit soit intercepter toutes les exceptions vérifiées que son corps de méthode peut lever, soit déclarer qu'elle lancera ces exceptions. La méthode principale d'Arcane3 appelle la méthode f sur l'objet de type statique Type3, mais elle ne gère pas

CloneNotSupportedException et InterruptedExceptioin. Alors, pourquoi ce programme peut-il compiler


  上述分析的缺陷在于对“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个问题: 主要比较"&操作符" 和 "&&操作符"的区别。注意示例中的操作符是&,这是按位进行"与"操作。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn