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

