Dieser Artikel sammelt und organisiert mehrere Rätsel zu Java-Anomalien für alle. Er ist sehr gut und hat Referenzwert.
Rätsel 1: Unentschlossen
Schauen Sie sich das Programm unten an. Was wird gedruckt?
public class Indecisive { public static void main(String[] args) { System.out.println(decision()); } private static boolean decision() { try { return true; } finally { return false; } } }
Laufendes Ergebnis:
falsch
Ergebnisbeschreibung:
In einer try-finally-Anweisung The Der ,finally-Anweisungsblock wird immer dann ausgeführt, wenn die Steuerung den try-Anweisungsblock verlässt. Dies gilt unabhängig davon, ob der Try-Block normal oder unerwartet endet.
Wenn eine Anweisung oder ein Anweisungsblock eine Ausnahme auslöst, eine Pause ausführt oder eine geschlossene Anweisung fortsetzt oder eine Rückgabe in einer Methode wie diesem Programm ausführt, kommt es zu einem unerwarteten Ende. Sie werden als unerwartete Abbrüche bezeichnet, da sie verhindern, dass das Programm die folgenden Anweisungen der Reihe nach ausführt. Wenn sowohl der Try-Block als auch der Final-Block unerwartet enden, wird der Grund für das unerwartete Ende im Try-Block verworfen, und der Grund für das unerwartete Ende der gesamten Try-finally-Anweisung ist derselbe wie der Grund für das unerwartete Ende des „finally“-Blocks. In diesem Programm wird das unerwartete Ende, das durch die Return-Anweisung im Try-Block verursacht wird, verworfen. Das unerwartete Ende der Try-finally-Anweisung wird durch die Return-Anweisung im Final-Block verursacht.
Einfach ausgedrückt: Das Programm versucht (versuchen) (zurückzugeben), true zurückzugeben, gibt aber letztendlich false (zurück). Das Verwerfen der Ursache für ein unerwartetes Ende ist fast nie ein wünschenswertes Verhalten, da die ursprüngliche Ursache für das unerwartete Ende möglicherweise wichtiger für das Verhalten des Programms zu sein scheint. Es ist besonders schwierig, das Verhalten von Programmen zu verstehen, die eine Break-, Continue- oder Return-Anweisung innerhalb eines Try-Blocks ausführen, nur um dann ihr Verhalten durch einen Final-Block außer Kraft zu setzen. Kurz gesagt: Jeder „finally“-Anweisungsblock sollte normal enden, es sei denn, es wird eine ungeprüfte Ausnahme ausgelöst. Verlassen Sie einen „finally“-Block niemals mit „return“, „break“, „continue“ oder „throw“ und lassen Sie niemals zu, dass eine geprüfte Ausnahme außerhalb eines „finally“-Blocks weitergegeben wird. Für Sprachdesigner sollte möglicherweise gefordert werden, dass der final-Anweisungsblock ohne eine ungeprüfte Ausnahme ordnungsgemäß beendet werden muss. Um dieses Ziel zu erreichen, erfordert das try-finally-Konstrukt, dass der final-Anweisungsblock normal enden kann. Return-, Break- oder Continue-Anweisungen, die die Kontrolle außerhalb eines „finally“-Blocks übertragen, sollten verboten sein, ebenso wie jede Anweisung, die eine geprüfte Ausnahme auslöst, die sich außerhalb eines „finally“-Blocks ausbreitet.
Rätsel 2: Extrem unglaublich
Was druckt jedes der drei Programme unten? Gehen Sie nicht davon aus, dass sie alle tun. Durch Zusammenstellung.
Erstes Programm
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!"); } } }
Zweites Programm
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"); } } }
Das dritte Programm
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(); } }
Ergebnisse:
(01) Das erste Programm wurde falsch kompiliert!
Arcane1.java:9: exception java.io.IOException is never thrown in body of corresponding try statement } catch(IOException e) { ^ error
(02) Das zweite Programm kann normal kompiliert und ausgeführt werden.
(03) Das dritte Programm kann normal kompiliert und ausgeführt werden. Das Ausgabeergebnis ist: Hallo Welt
Ergebnisbeschreibung:
(01) Arcane1 demonstriert ein Grundprinzip geprüfter Ausnahmen. Es sieht so aus, als ob es kompiliert werden sollte: Die Try-Klausel führt E/A aus und die Catch-Klausel fängt IOException-Ausnahmen ab. Dieses Programm kann jedoch nicht kompiliert werden, da die println-Methode nicht zum Auslösen einer geprüften Ausnahme deklariert ist und IOException genau eine geprüfte Ausnahme ist. Die Sprachspezifikation beschreibt: Wenn eine Catch-Klausel eine geprüfte Ausnahme vom Typ E abfängt und die entsprechende Try-Klausel keine Ausnahme eines Untertyps von E auslösen kann, handelt es sich um einen Fehler bei der Kompilierung.
(02) Aus dem gleichen Grund scheint das zweite Programm, Arcane2, nicht kompiliert zu werden, tut es aber. Es wird kompiliert, weil seine einzige Catch-Klausel nach Ausnahmen sucht. Obwohl es an dieser Stelle noch sehr unklar ist, ist eine Catch-Klausel, die eine Ausnahme oder einen Throwble abfängt, zulässig, unabhängig vom Inhalt der entsprechenden Try-Klausel. Obwohl Arcane2 ein legales Programm ist, wird der Inhalt der Catch-Klausel niemals ausgeführt und das Programm gibt nichts aus.
(03) Das dritte Programm, Arcane3, scheint sich ebenfalls nicht kompilieren zu lassen. Die Methode f wird in der Type1-Schnittstelle deklariert, um eine geprüfte Ausnahme CloneNotSupportedException auszulösen, und wird in der Type2-Schnittstelle deklariert, um eine geprüfte Ausnahme InterruptedException auszulösen. Die Type3-Schnittstelle erbt von Type1 und Type2, daher scheint es, dass der Aufruf der Methode f für ein Objekt vom statischen Typ Type3 möglicherweise diese Ausnahmen auslöst. Eine Methode muss entweder alle geprüften Ausnahmen abfangen, die ihr Methodenkörper auslösen kann, oder deklarieren, dass sie diese Ausnahmen auslösen wird. Die Hauptmethode von Arcane3 ruft die Methode f für das Objekt des statischen Typs Type3 auf, verarbeitet jedoch
CloneNotSupportedException und InterruptedExceptioin nicht. Warum kann dieses Programm also kompiliert werden?
上述分析的缺陷在于对“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个问题: 主要比较"&操作符" 和 "&&操作符"的区别。注意示例中的操作符是&,这是按位进行"与"操作。
Das obige ist der detaillierte Inhalt vonDetaillierte Analyse mehrerer Rätsel zu Java-Ausnahmen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!