Java异常的几个谜题_动力节点Java学院整理
谜题1:优柔寡断
看看下面的程序,它到底打印什么?
publicclassIndecisive{ publicstaticvoidmain(String[]args){ System.out.println(decision()); } privatestaticbooleandecision(){ try{ returntrue; }finally{ returnfalse; } } }
运行结果:
false
结果说明:
在一个try-finally语句中,finally语句块总是在控制权离开try语句块时执行的。无论try语句块是正常结束的,还是意外结束的,情况都是如此。
一条语句或一个语句块在它抛出了一个异常,或者对某个封闭型语句执行了一个break或continue,或是象这个程序一样在方法中执行了一个return时,将发生意外结束。它们之所以被称为意外结束,是因为它们阻止程序去按顺序执行下面的语句。当try语句块和finally语句块都意外结束时,try语句块中引发意外结束的原因将被丢弃,而整个try-finally语句意外结束的原因将于finally语句块意外结束的原因相同。在这个程序中,在try语句块中的return语句所引发的意外结束将被丢弃,try-finally语句意外结束是由finally语句块中的return而造成的。
简单地讲,程序尝试着(try)(return)返回true,但是它最终(finally)返回(return)的是false。丢弃意外结束的原因几乎永远都不是你想要的行为,因为意外结束的最初原因可能对程序的行为来说会显得更重要。对于那些在try语句块中执行break、continue或return语句,只是为了使其行为被finally语句块所否决掉的程序,要理解其行为是特别困难的。总之,每一个finally语句块都应该正常结束,除非抛出的是不受检查的异常。千万不要用一个return、break、continue或throw来退出一个finally语句块,并且千万不要允许将一个受检查的异常传播到一个finally语句块之外去。对于语言设计者,也许应该要求finally语句块在未出现不受检查的异常时必须正常结束。朝着这个目标,try-finally结构将要求finally语句块可以正常结束。return、break或continue语句把控制权传递到finally语句块之外应该是被禁止的,任何可以引发将被检查异常传播到finally语句块之外的语句也同样应该是被禁止的。
谜题2:极端不可思议
下面的三个程序每一个都会打印些什么?不要假设它们都可以通过编译。
第一个程序
importjava.io.IOException; publicclassArcane1{ publicstaticvoidmain(String[]args){ try{ System.out.println("Helloworld"); }catch(IOExceptione){ System.out.println("I'veneverseenprintlnfail!"); } } }
第二个程序
publicclassArcane2{ publicstaticvoidmain(String[]args){ try{ //Ifyouhavenothingnicetosay,saynothing }catch(Exceptione){ System.out.println("Thiscan'thappen"); } } }
第三个程序
interfaceType1{ voidf()throwsCloneNotSupportedException; } interfaceType2{ voidf()throwsInterruptedException; } interfaceType3extendsType1,Type2{ } publicclassArcane3implementsType3{ publicvoidf(){ System.out.println("Helloworld"); } publicstaticvoidmain(String[]args){ Type3t3=newArcane3(); t3.f(); } }
运行结果:
(01)第一个程序编译出错!
Arcane1.java:9:exceptionjava.io.IOExceptionisneverthrowninbodyofcorrespondingtrystatement }catch(IOExceptione){ ^ error
(02)第二个程序能正常编译和运行。
(03)第三个程序能正常编译和运行。输出结果是: Helloworld
结果说明:
(01)Arcane1展示了被检查异常的一个基本原则。它看起来应该是可以编译的:try子句执行I/O,并且catch子句捕获IOException异常。但是这个程序不能编译,因为println方法没有声明会抛出任何被检查异常,而IOException却正是一个被检查异常。语言规范中描述道:如果一个catch子句要捕获一个类型为E的被检查异常,而其相对应的try子句不能抛出E的某种子类型的异常,那么这就是一个编译期错误。
(02)基于同样的理由,第二个程序,Arcane2,看起来应该是不可以编译的,但是它却可以。它之所以可以编译,是因为它唯一的catch子句检查了Exception。尽管在这一点上十分含混不清,但是捕获Exception或Throwble的catch子句是合法的,不管与其相对应的try子句的内容为何。尽管Arcane2是一个合法的程序,但是catch子句的内容永远的不会被执行,这个程序什么都不会打印。
(03)第三个程序,Arcane3,看起来它也不能编译。方法f在Type1接口中声明要抛出被检查异常CloneNotSupportedException,并且在Type2接口中声明要抛出被检查异常InterruptedException。Type3接口继承了Type1和Type2,因此,看起来在静态类型为Type3的对象上调用方法f时,有潜在可能会抛出这些异常。一个方法必须要么捕获其方法体可以抛出的所有被检查异常,要么声明它将抛出这些异常。Arcane3的main方法在静态类型为Type3的对象上调用了方法f,但它对
CloneNotSupportedException和InterruptedExceptioin并没有作这些处理。那么,为什么这个程序可以编译呢?
上述分析的缺陷在于对“Type3.f可以抛出在Type1.f上声明的异常和在Type2.f上声明的异常”所做的假设。这并不正确,因为每一个接口都限制了方法f可以抛出的被检查异常集合。一个方法可以抛出的被检查异常集合是它所适用的所有类型声明要抛出的被检查异常集合的交集,而不是合集。因此,静态类型为Type3的对象上的f方法根本就不能抛出任何被检查异常。因此,Arcane3可以毫无错误地通过编译,并且打印Helloworld。
谜题3:不受欢迎的宾客
下面的程序会打印出什么呢?
publicclassUnwelcomeGuest{ publicstaticfinallongGUEST_USER_ID=-1; privatestaticfinallongUSER_ID; static{ try{ USER_ID=getUserIdFromEnvironment(); }catch(IdUnavailableExceptione){ USER_ID=GUEST_USER_ID; System.out.println("Logginginasguest"); } } privatestaticlonggetUserIdFromEnvironment() throwsIdUnavailableException{ thrownewIdUnavailableException(); } publicstaticvoidmain(String[]args){ System.out.println("UserID:"+USER_ID); } } classIdUnavailableExceptionextendsException{ }
运行结果:
UnwelcomeGuest.java:10:variableUSER_IDmightalreadyhavebeenassigned USER_ID=GUEST_USER_ID; ^ error
结果说明:
该程序看起来很直观。对getUserIdFromEnvironment的调用将抛出一个异常,从而使程序将GUEST_USER_ID(-1L)赋值给USER_ID,并打印Loggininasguest。然后main方法执行,使程序打印UserID:-1。表象再次欺骗了我们,该程序并不能编译。如果你尝试着去编译它,你将看到和一条错误信息。
问题出在哪里了?USER_ID域是一个空final(blankfinal),它是一个在声明中没有进行初始化操作的final域。很明显,只有在对USER_ID赋值失败时,才会在try语句块中抛出异常,因此,在catch语句块中赋值是相当安全的。不管怎样执行静态初始化操作语句块,只会对USER_ID赋值一次,这正是空final所要求的。为什么编译器不知道这些呢?要确定一个程序是否可以不止一次地对一个空final进行赋值是一个很困难的问题。事实上,这是不可能的。这等价于经典的停机问题,它通常被认为是不可能解决的。为了能够编写出一个编译器,语言规范在这一点上采用了保守的方式。在程序中,一个空final域只有在它是明确未赋过值的地方才可以被赋值。规范长篇大论,对此术语提供了一个准确的但保守的定义。因为它是保守的,所以编译器必须拒绝某些可以证明是安全的程序。这个谜题就展示了这样的一个程序。幸运的是,你不必为了编写Java程序而去学习那些骇人的用于明确赋值的细节。通常明确赋值规则不会有任何妨碍。如果碰巧你编写了一个真的可能会对一个空final赋值超过一次的程序,编译器会帮你指出的。只有在极少的情况下,就像本谜题一样,你才会编写出一个安全的程序,但是它并不满足规范的形式化要求。编译器的抱怨就好像是你编写了一个不安全的程序一样,而且你必须修改你的程序以满足它。
解决这类问题的最好方式就是将这个烦人的域从空final类型改变为普通的final类型,用一个静态域的初始化操作替换掉静态的初始化语句块。实现这一点的最佳方式是重构静态语句块中的代码为一个助手方法:
publicclassUnwelcomeGuest{ publicstaticfinallongGUEST_USER_ID=-1; privatestaticfinallongUSER_ID=getUserIdOrGuest(); privatestaticlonggetUserIdOrGuest(){ try{ returngetUserIdFromEnvironment(); }catch(IdUnavailableExceptione){ System.out.println("Logginginasguest"); returnGUEST_USER_ID; } } privatestaticlonggetUserIdFromEnvironment() throwsIdUnavailableException{ thrownewIdUnavailableException(); } publicstaticvoidmain(String[]args){ System.out.println("UserID:"+USER_ID); } } classIdUnavailableExceptionextendsException{ }
程序的这个版本很显然是正确的,而且比最初的版本根据可读性,因为它为了域值的计算而增加了一个描述性的名字,而最初的版本只有一个匿名的静态初始化操作语句块。将这样的修改作用于程序,它就可以如我们的期望来运行了。总之,大多数程序员都不需要学习明确赋值规则的细节。该规则的作为通常都是正确的。如果你必须重构一个程序,以消除由明确赋值规则所引发的错误,那么你应该考虑添加一个新方法。这样做除了可以解决明确赋值问题,还可以使程序的可读性提高。
谜题4:您好,再见!
下面的程序将会打印出什么呢?
publicclassHelloGoodbye{ publicstaticvoidmain(String[]args){ try{ System.out.println("Helloworld"); System.exit(0); }finally{ System.out.println("Goodbyeworld"); } } }
运行结果:
Helloworld
结果说明:
这个程序包含两个println语句:一个在try语句块中,另一个在相应的finally语句块中。try语句块执行它的println语句,并且通过调用System.exit来提前结束执行。在此时,你可能希望控制权会转交给finally语句块。然而,如果你运行该程序,就会发现它永远不会说再见:它只打印了Helloworld。这是否违背了"Indecisive示例"中所解释的原则呢?不论try语句块的执行是正常地还是意外地结束,finally语句块确实都会执行。然而在这个程序中,try语句块根本就没有结束其执行过程。System.exit方法将停止当前线程和所有其他当场死亡的线程。finally子句的出现并不能给予线程继续去执行的特殊权限。
当System.exit被调用时,虚拟机在关闭前要执行两项清理工作。首先,它执行所有的关闭挂钩操作,这些挂钩已经注册到了Runtime.addShutdownHook上。这对于释放VM之外的资源将很有帮助。务必要为那些必须在VM退出之前发生的行为关闭挂钩。下面的程序版本示范了这种技术,它可以如我们所期望地打印出Helloworld和Goodbyeworld:
publicclassHelloGoodbye1{ publicstaticvoidmain(String[]args){ System.out.println("Helloworld"); Runtime.getRuntime().addShutdownHook( newThread(){ publicvoidrun(){ System.out.println("Goodbyeworld"); } }); 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:不情愿的构造器
下面的程序将打印出什么呢?
publicclassReluctant{ privateReluctantinternalInstance=newReluctant(); publicReluctant()throwsException{ thrownewException("I'mnotcomingout"); } publicstaticvoidmain(String[]args){ try{ Reluctantb=newReluctant(); System.out.println("Surprise!"); }catch(Exceptionex){ System.out.println("Itoldyouso"); } } }
运行结果:
Exceptioninthread"main"java.lang.StackOverflowError atReluctant.(Reluctant.java:3) ...
结果说明:
main方法调用了Reluctant构造器,它将抛出一个异常。你可能期望catch子句能够捕获这个异常,并且打印Itoldyouso。凑近仔细看看这个程序就会发现,Reluctant实例还包含第二个内部实例,它的构造器也会抛出一个异常。无论抛出哪一个异常,看起来main中的catch子句都应该捕获它,因此预测该程序将打印Itoldyou应该是一个安全的赌注。但是当你尝试着去运行它时,就会发现它压根没有去做这类的事情:它抛出了StackOverflowError异常,为什么呢?
与大多数抛出StackOverflowError异常的程序一样,本程序也包含了一个无限递归。当你调用一个构造器时,实例变量的初始化操作将先于构造器的程序体而运行[JLS12.5]。在本谜题中,internalInstance变量的初始化操作递归调用了构造器,而该构造器通过再次调用Reluctant构造器而初始化该变量自己的internalInstance域,如此无限递归下去。这些递归调用在构造器程序体获得执行机会之前就会抛出StackOverflowError异常,因为StackOverflowError是Error的子类型而不是Exception的子类型,所以catch子句无法捕获它。对于一个对象包含与它自己类型相同的实例的情况,并不少见。例如,链接列表节点、树节点和图节点都属于这种情况。你必须非常小心地初始化这样的包含实例,以避免StackOverflowError异常。
至于本谜题名义上的题目:声明将抛出异常的构造器,你需要注意,构造器必须声明其实例初始化操作会抛出的所有被检查异常。
谜题6:域和流
下面的方法将一个文件拷贝到另一个文件,并且被设计为要关闭它所创建的每一个流,即使它碰到I/O错误也要如此。遗憾的是,它并非总是能够做到这一点。为什么不能呢,你如何才能订正它呢?
staticvoidcopy(Stringsrc,Stringdest)throwsIOException{ InputStreamin=null; OutputStreamout=null; try{ in=newFileInputStream(src); out=newFileOutputStream(dest); byte[]buf=newbyte[1024]; intn; 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(IOExceptionex){ //Thereisnothingwecandoifclosefails } } if(out!=null){ try{ out.close(); }catch(IOExceptionex){ //Thereisnothingwecandoifclosefails } } }
总之,当你在finally语句块中调用close方法时,要用一个嵌套的try-catch语句来保护它,以防止IOException的传播。更一般地讲,对于任何在finally语句块中可能会抛出的被检查异常都要进行处理,而不是任其传播。
谜题7:异常为循环而抛
下面的程序会打印出什么呢?
publicclassLoop{ publicstaticvoidmain(String[]args){ int[][]tests={{6,5,4,3,2,1},{1,2}, {1,2,3},{1,2,3,4},{1}}; intsuccessCount=0; try{ inti=0; while(true){ if(thirdElementIsThree(tests[i++])) successCount++; } }catch(ArrayIndexOutOfBoundsExceptione){ //Nomoreteststoprocess } System.out.println(successCount); } privatestaticbooleanthirdElementIsThree(int[]a){ returna.length>=3&a[2]==3; } }
运行结果:
0
结果说明:
该程序主要说明了两个问题。
第1个问题:不应该使用异常作为终止循环的手段!
该程序用thirdElementIsThree方法测试了tests数组中的每一个元素。遍历这个数组的循环显然是非传统的循环:它不是在循环变量等于数组长度的时候终止,而是在它试图访问一个并不在数组中的元素时终止。尽管它是非传统的,但是这个循环应该可以工作。
如果传递给thirdElementIsThree的参数具有3个或更多的元素,并且其第三个元素等于3,那么该方法将返回true。对于tests中的5个元素来说,有2个将返回true,因此看起来该程序应该打印2。如果你运行它,就会发现它打印的时0。肯定是哪里出了问题,你能确定吗?事实上,这个程序犯了两个错误。第一个错误是该程序使用了一种可怕的循环惯用法,该惯用法依赖的是对数组的访问会抛出异常。这种惯用法不仅难以阅读,而且运行速度还非常地慢。不要使用异常来进行循环控制;应该只为异常条件而使用异常。为了纠正这个错误,可以将整个try-finally语句块替换为循环遍历数组的标准惯用法:
for(inti=0;i如果你使用的是5.0或者是更新的版本,那么你可以用for循环结构来代替:
for(int[]test:tests) if(thirdElementIsThree(test)) successCount++;第2个问题:主要比较"&操作符"和"&&操作符"的区别。注意示例中的操作符是&,这是按位进行"与"操作。
以上所述是小编给大家介绍的Java异常的几个谜题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!