Java異常的幾個謎題_動力節(jié)點(diǎn)Java學(xué)院整理
謎題1: 優(yōu)柔寡斷
看看下面的程序,它到底打印什么?
public class Indecisive { public static void main(String[] args) { System.out.println(decision()); } private static boolean decision() { try { return true; } finally { return false; } } }
運(yùn)行結(jié)果:
false
結(jié)果說明:
在一個 try-finally 語句中,finally 語句塊總是在控制權(quán)離開 try 語句塊時執(zhí)行的。無論 try 語句塊是正常結(jié)束的,還是意外結(jié)束的, 情況都是如此。
一條語句或一個語句塊在它拋出了一個異常,或者對某個封閉型語句執(zhí)行了一個 break 或 continue,或是象這個程序一樣在方法中執(zhí)行了一個return 時,將發(fā)生意外結(jié)束。它們之所以被稱為意外結(jié)束,是因為它們阻止程序去按順序執(zhí)行下面的語句。當(dāng) try 語句塊和 finally 語句塊都意外結(jié)束時, try 語句塊中引發(fā)意外結(jié)束的原因?qū)⒈粊G棄, 而整個 try-finally 語句意外結(jié)束的原因?qū)⒂?finally 語句塊意外結(jié)束的原因相同。在這個程序中,在 try 語句塊中的 return 語句所引發(fā)的意外結(jié)束將被丟棄, try-finally 語句意外結(jié)束是由 finally 語句塊中的 return 而造成的。
簡單地講, 程序嘗試著 (try) (return) 返回 true, 但是它最終 (finally) 返回(return)的是 false。丟棄意外結(jié)束的原因幾乎永遠(yuǎn)都不是你想要的行為, 因為意外結(jié)束的最初原因可能對程序的行為來說會顯得更重要。對于那些在 try 語句塊中執(zhí)行 break、continue 或 return 語句,只是為了使其行為被 finally 語句塊所否決掉的程序,要理解其行為是特別困難的。總之,每一個 finally 語句塊都應(yīng)該正常結(jié)束,除非拋出的是不受檢查的異常。 千萬不要用一個 return、break、continue 或 throw 來退出一個 finally 語句塊,并且千萬不要允許將一個受檢查的異常傳播到一個 finally 語句塊之外去。對于語言設(shè)計者, 也許應(yīng)該要求 finally 語句塊在未出現(xiàn)不受檢查的異常時必須正常結(jié)束。朝著這個目標(biāo),try-finally 結(jié)構(gòu)將要求 finally 語句塊可以正常結(jié)束。return、break 或 continue 語句把控制權(quán)傳遞到 finally 語句塊之外應(yīng)該是被禁止的, 任何可以引發(fā)將被檢查異常傳播到 finally 語句塊之外的語句也同樣應(yīng)該是被禁止的。
謎題2: 極端不可思議
下面的三個程序每一個都會打印些什么? 不要假設(shè)它們都可以通過編譯。
第一個程序
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(); } }
運(yùn)行結(jié)果:
(01) 第一個程序編譯出錯!
Arcane1.java:9: exception java.io.IOException is never thrown in body of corresponding try statement } catch(IOException e) { ^ error
(02) 第二個程序能正常編譯和運(yùn)行。
(03) 第三個程序能正常編譯和運(yùn)行。輸出結(jié)果是: Hello world
結(jié)果說明:
(01) Arcane1展示了被檢查異常的一個基本原則。它看起來應(yīng)該是可以編譯的:try 子句執(zhí)行 I/O,并且 catch 子句捕獲 IOException 異常。但是這個程序不能編譯,因為 println 方法沒有聲明會拋出任何被檢查異常,而IOException 卻正是一個被檢查異常。語言規(guī)范中描述道:如果一個 catch 子句要捕獲一個類型為 E 的被檢查異常, 而其相對應(yīng)的 try 子句不能拋出 E 的某種子類型的異常,那么這就是一個編譯期錯誤。
(02) 基于同樣的理由,第二個程序,Arcane2,看起來應(yīng)該是不可以編譯的,但是它卻可以。它之所以可以編譯,是因為它唯一的 catch 子句檢查了 Exception。盡管在這一點(diǎn)上十分含混不清,但是捕獲 Exception 或 Throwble 的 catch 子句是合法的,不管與其相對應(yīng)的 try 子句的內(nèi)容為何。盡管 Arcane2 是一個合法的程序,但是 catch 子句的內(nèi)容永遠(yuǎn)的不會被執(zhí)行,這個程序什么都不會打印。
(03) 第三個程序,Arcane3,看起來它也不能編譯。方法 f 在 Type1 接口中聲明要拋出被檢查異常 CloneNotSupportedException,并且在 Type2 接口中聲明要拋出被檢查異常 InterruptedException。Type3 接口繼承了 Type1 和 Type2,因此, 看起來在靜態(tài)類型為 Type3 的對象上調(diào)用方法 f 時, 有潛在可能會拋出這些異常。一個方法必須要么捕獲其方法體可以拋出的所有被檢查異常, 要么聲明它將拋出這些異常。Arcane3 的 main 方法在靜態(tài)類型為 Type3 的對象上調(diào)用了方法 f,但它對
CloneNotSupportedException 和 InterruptedExceptioin 并沒有作這些處理。那么,為什么這個程序可以編譯呢?
上述分析的缺陷在于對“Type3.f 可以拋出在 Type1.f 上聲明的異常和在 Type2.f 上聲明的異?!彼龅募僭O(shè)。這并不正確,因為每一個接口都限制了方法 f 可以拋出的被檢查異常集合。一個方法可以拋出的被檢查異常集合是它所適用的所有類型聲明要拋出的被檢查異常集合的交集,而不是合集。因此,靜態(tài)類型為 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 { }
運(yùn)行結(jié)果:
UnwelcomeGuest.java:10: variable USER_ID might already have been assigned USER_ID = GUEST_USER_ID; ^ error
結(jié)果說明:
該程序看起來很直觀。對 getUserIdFromEnvironment 的調(diào)用將拋出一個異常, 從而使程序?qū)?GUEST_USER_ID(-1L)賦值給 USER_ID, 并打印 Loggin in as guest。 然后 main 方法執(zhí)行,使程序打印 User ID: -1。表象再次欺騙了我們,該程序并不能編譯。如果你嘗試著去編譯它, 你將看到和一條錯誤信息。
問題出在哪里了?USER_ID 域是一個空 final(blank final),它是一個在聲明中沒有進(jìn)行初始化操作的 final 域。很明顯,只有在對 USER_ID 賦值失敗時,才會在 try 語句塊中拋出異常,因此,在 catch 語句塊中賦值是相 當(dāng)安全的。不管怎樣執(zhí)行靜態(tài)初始化操作語句塊,只會對 USER_ID 賦值一次,這正是空 final 所要求的。為什么編譯器不知道這些呢? 要確定一個程序是否可以不止一次地對一個空 final 進(jìn)行賦值是一個很困難的問題。事實上,這是不可能的。這等價于經(jīng)典的停機(jī)問題,它通常被認(rèn)為是不可能解決的。為了能夠編寫出一個編譯器,語言規(guī)范在這一點(diǎn)上采用了保守的方式。在程序中,一個空 final 域只有在它是明確未賦過值的地方才可以被賦值。規(guī)范長篇大論,對此術(shù)語提供了一個準(zhǔn)確的但保守的定義。 因為它是保守的,所以編譯器必須拒絕某些可以證明是安全的程序。這個謎題就展示了這樣的一個程序。幸運(yùn)的是, 你不必為了編寫 Java 程序而去學(xué)習(xí)那些駭人的用于明確賦值的細(xì)節(jié)。通常明確賦值規(guī)則不會有任何妨礙。如果碰巧你編寫了一個真的可能會對一個空final 賦值超過一次的程序,編譯器會幫你指出的。只有在極少的情況下,就像本謎題一樣, 你才會編寫出一個安全的程序, 但是它并不滿足規(guī)范的形式化要求。編譯器的抱怨就好像是你編寫了一個不安全的程序一樣,而且你必須修改你的程序以滿足它。
解決這類問題的最好方式就是將這個煩人的域從空 final 類型改變?yōu)槠胀ǖ膄inal 類型,用一個靜態(tài)域的初始化操作替換掉靜態(tài)的初始化語句塊。實現(xiàn)這一點(diǎn)的最佳方式是重構(gòu)靜態(tài)語句塊中的代碼為一個助手方法:
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 { }
程序的這個版本很顯然是正確的,而且比最初的版本根據(jù)可讀性,因為它為了域值的計算而增加了一個描述性的名字, 而最初的版本只有一個匿名的靜態(tài)初始化操作語句塊。將這樣的修改作用于程序,它就可以如我們的期望來運(yùn)行了??傊?大多數(shù)程序員都不需要學(xué)習(xí)明確賦值規(guī)則的細(xì)節(jié)。該規(guī)則的作為通常都是正確的。如果你必須重構(gòu)一個程序,以消除由明確賦值規(guī)則所引發(fā)的錯誤,那么你應(yīng)該考慮添加一個新方法。這樣做除了可以解決明確賦值問題,還可以使程序的可讀性提高。
謎題4: 您好,再見!
下面的程序?qū)蛴〕鍪裁茨?#63;
public class HelloGoodbye { public static void main(String[] args) { try { System.out.println("Hello world"); System.exit(0); } finally { System.out.println("Goodbye world"); } } }
運(yùn)行結(jié)果:
Hello world
結(jié)果說明:
這個程序包含兩個 println 語句: 一個在 try 語句塊中, 另一個在相應(yīng)的 finally語句塊中。try 語句塊執(zhí)行它的 println 語句,并且通過調(diào)用 System.exit 來提前結(jié)束執(zhí)行。在此時,你可能希望控制權(quán)會轉(zhuǎn)交給 finally 語句塊。然而,如果你運(yùn)行該程序,就會發(fā)現(xiàn)它永遠(yuǎn)不會說再見:它只打印了 Hello world。這是否違背了"Indecisive示例" 中所解釋的原則呢? 不論 try 語句塊的執(zhí)行是正常地還是意外地結(jié)束, finally 語句塊確實都會執(zhí)行。然而在這個程序中,try 語句塊根本就沒有結(jié)束其執(zhí)行過程。System.exit 方法將停止當(dāng)前線程和所有其他當(dāng)場死亡的線程。finally 子句的出現(xiàn)并不能給予線程繼續(xù)去執(zhí)行的特殊權(quán)限。
當(dāng) System.exit 被調(diào)用時,虛擬機(jī)在關(guān)閉前要執(zhí)行兩項清理工作。首先,它執(zhí)行所有的關(guān)閉掛鉤操作,這些掛鉤已經(jīng)注冊到了 Runtime.addShutdownHook 上。這對于釋放 VM 之外的資源將很有幫助。務(wù)必要為那些必須在 VM 退出之前發(fā)生的行為關(guān)閉掛鉤。下面的程序版本示范了這種技術(shù),它可以如我們所期望地打印出 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 執(zhí)行在 System.exit 被調(diào)用時執(zhí)行的第二個清理任務(wù)與終結(jié)器有關(guān)。如果System.runFinalizerOnExit 或它的魔鬼雙胞胎 Runtime.runFinalizersOnExit被調(diào)用了,那么 VM 將在所有還未終結(jié)的對象上面調(diào)用終結(jié)器。這些方法很久以前就已經(jīng)過時了,而且其原因也很合理。無論什么原因,永遠(yuǎn)不要調(diào)用System.runFinalizersOnExit 和 Runtime.runFinalizersOnExit: 它們屬于 Java類庫中最危險的方法之一[ThreadStop]。調(diào)用這些方法導(dǎo)致的結(jié)果是,終結(jié)器會在那些其他線程正在并發(fā)操作的對象上面運(yùn)行, 從而導(dǎo)致不確定的行為或?qū)е滤梨i。
總之,System.exit 將立即停止所有的程序線程,它并不會使 finally 語句塊得到調(diào)用,但是它在停止 VM 之前會執(zhí)行關(guān)閉掛鉤操作。當(dāng) VM 被關(guān)閉時,請使用關(guān)閉掛鉤來終止外部資源。通過調(diào)用 System.halt 可以在不執(zhí)行關(guān)閉掛鉤的情況下停止 VM,但是這個方法很少使用。
謎題5: 不情愿的構(gòu)造器
下面的程序?qū)⒋蛴〕鍪裁茨?#63;
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"); } } }
運(yùn)行結(jié)果:
Exception in thread "main" java.lang.StackOverflowError at Reluctant.<init>(Reluctant.java:3) ...
結(jié)果說明:
main 方法調(diào)用了 Reluctant 構(gòu)造器,它將拋出一個異常。你可能期望 catch 子句能夠捕獲這個異常,并且打印 I told you so。湊近仔細(xì)看看這個程序就會發(fā)現(xiàn),Reluctant 實例還包含第二個內(nèi)部實例,它的構(gòu)造器也會拋出一個異常。無論拋出哪一個異常,看起來 main 中的 catch 子句都應(yīng)該捕獲它,因此預(yù)測該程序?qū)⒋蛴?I told you 應(yīng)該是一個安全的賭注。但是當(dāng)你嘗試著去運(yùn)行它時,就會發(fā)現(xiàn)它壓根沒有去做這類的事情:它拋出了 StackOverflowError 異常,為什么呢?
與大多數(shù)拋出 StackOverflowError 異常的程序一樣,本程序也包含了一個無限遞歸。當(dāng)你調(diào)用一個構(gòu)造器時,實例變量的初始化操作將先于構(gòu)造器的程序體而運(yùn)行[JLS 12.5]。在本謎題中, internalInstance 變量的初始化操作遞歸調(diào)用了構(gòu)造器,而該構(gòu)造器通過再次調(diào)用 Reluctant 構(gòu)造器而初始化該變量自己的 internalInstance 域,如此無限遞歸下去。這些遞歸調(diào)用在構(gòu)造器程序體獲得執(zhí)行機(jī)會之前就會拋出 StackOverflowError 異常,因為 StackOverflowError 是 Error 的子類型而不是 Exception 的子類型,所以 catch 子句無法捕獲它。對于一個對象包含與它自己類型相同的實例的情況,并不少見。例如,鏈接列表節(jié)點(diǎn)、樹節(jié)點(diǎn)和圖節(jié)點(diǎn)都屬于這種情況。你必須非常小心地初始化這樣的包含實例,以避免 StackOverflowError 異常。
至于本謎題名義上的題目:聲明將拋出異常的構(gòu)造器,你需要注意,構(gòu)造器必須聲明其實例初始化操作會拋出的所有被檢查異常。
謎題6: 域和流
下面的方法將一個文件拷貝到另一個文件,并且被設(shè)計為要關(guān)閉它所創(chuàng)建的每一個流,即使它碰到 I/O 錯誤也要如此。遺憾的是,它并非總是能夠做到這一點(diǎn)。為什么不能呢,你如何才能訂正它呢?
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(); } }
謎題分析:
這個程序看起來已經(jīng)面面俱到了。其流域(in 和 out)被初始化為 null,并且新的流一旦被創(chuàng)建,它們馬上就被設(shè)置為這些流域的新值。對于這些域所引用的流,如果不為空,則 finally 語句塊會將其關(guān)閉。即便在拷貝操作引發(fā)了一個 IOException 的情況下,finally 語句塊也會在方法返回之前執(zhí)行。出什么錯了呢?
問題在 finally 語句塊自身中。close 方法也可能會拋出 IOException 異常。如果這正好發(fā)生在 in.close 被調(diào)用之時,那么這個異常就會阻止 out.close 被調(diào)用,從而使輸出流仍保持在開放狀態(tài)。請注意,該程序違反了"優(yōu)柔寡斷" 的建議:對 close 的調(diào)用可能會導(dǎo)致 finally 語句塊意外結(jié)束。遺憾的是,編譯器并不能幫助你發(fā)現(xiàn)此問題,因為 close 方法拋出的異常與 read 和 write 拋出的異常類型相同,而其外圍方法(copy)聲明將傳播該異常。解決方式是將每一個 close 都包裝在一個嵌套的 try 語句塊中。
下面的 finally 語句塊的版本可以保證在兩個流上都會調(diào)用 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 } } }
總之,當(dāng)你在 finally 語句塊中調(diào)用 close 方法時,要用一個嵌套的 try-catch 語句來保護(hù)它,以防止 IOException 的傳播。更一般地講,對于任何在 finally 語句塊中可能會拋出的被檢查異常都要進(jìn)行處理,而不是任其傳播。
謎題7: 異常為循環(huán)而拋
下面的程序會打印出什么呢?
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; } }
運(yùn)行結(jié)果:
0
結(jié)果說明:
該程序主要說明了兩個問題。
第1個問題:不應(yīng)該使用異常作為終止循環(huán)的手段!
該程序用 thirdElementIsThree 方法測試了 tests 數(shù)組中的每一個元素。遍歷這個數(shù)組的循環(huán)顯然是非傳統(tǒng)的循環(huán):它不是在循環(huán)變量等于數(shù)組長度的時候終止,而是在它試圖訪問一個并不在數(shù)組中的元素時終止。盡管它是非傳統(tǒng)的,但是這個循環(huán)應(yīng)該可以工作。
如果傳遞給 thirdElementIsThree 的參數(shù)具有 3 個或更多的元素,并且其第三個元素等于 3,那么該方法將返回 true。對于 tests中的 5 個元素來說,有 2 個將返回 true,因此看起來該程序應(yīng)該打印 2。如果你運(yùn)行它,就會發(fā)現(xiàn)它打印的時 0。肯定是哪里出了問題,你能確定嗎? 事實上,這個程序犯了兩個錯誤。第一個錯誤是該程序使用了一種可怕的循環(huán)慣用法,該慣用法依賴的是對數(shù)組的訪問會拋出異常。這種慣用法不僅難以閱讀, 而且運(yùn)行速度還非常地慢。不要使用異常來進(jìn)行循環(huán)控制;應(yīng)該只為異常條件而使用異常。為了糾正這個錯誤,可以將整個 try-finally 語句塊替換為循環(huán)遍歷數(shù)組的標(biāo)準(zhǔn)慣用法:
for (int i = 0; i < test.length; i++) if (thirdElementIsThree(tests[i])) successCount++;
如果你使用的是 5.0 或者是更新的版本,那么你可以用 for 循環(huán)結(jié)構(gòu)來代替:
for (int[] test : tests) if(thirdElementIsThree(test)) successCount++;
第2個問題: 主要比較"&操作符" 和 "&&操作符"的區(qū)別。注意示例中的操作符是&,這是按位進(jìn)行"與"操作。
以上所述是小編給大家介紹的Java異常的幾個謎題,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
解決java.lang.ClassCastException的java類型轉(zhuǎn)換異常的問題
這篇文章主要介紹了解決java.lang.ClassCastException的java類型轉(zhuǎn)換異常的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09利用Kotlin + Spring Boot實現(xiàn)后端開發(fā)
這篇文章主要給大家介紹了關(guān)于利用Kotlin + Spring Boot實現(xiàn)后端開發(fā)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11java學(xué)習(xí)筆記之DBUtils工具包詳解
下面小編就為大家分享一篇java學(xué)習(xí)筆記之DBUtils工具包詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01