JAVA異常謎題38:不受歡迎的賓客

字號(hào):

本謎題中的程序所建模的系統(tǒng),將嘗試著從其環(huán)境中讀取一個(gè)用戶ID,如果這種嘗試失敗了,則缺省地認(rèn)為它是一個(gè)來賓用戶。該程序的作者將面對(duì)有一個(gè)靜態(tài)域的初始化表達(dá)式可能會(huì)拋出異常的情況。因?yàn)镴ava不允許靜態(tài)初始化操作拋出被檢查異常,所以初始化必須包裝在try-finally語句塊中。那么,下面的程序會(huì)打印出什么呢?
    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 {
    }
    該程序看起來很直觀。對(duì)getUserIdFromEnvironment的調(diào)用將拋出一個(gè)異常,從而使程序?qū)UEST_USER_ID(-1L)賦值給USER_ID,并打印Loggin in as guest。然后main方法執(zhí)行,使程序打印User ID: -1。表象再次欺騙了我們,該程序并不能編譯。如果你嘗試著去編譯它,你將看到和下面內(nèi)容類似的一條錯(cuò)誤信息:
    UnwelcomeGuest.java:10:
    variable USER_ID might already have been assigned
     USER_ID = GUEST_USER_ID;
     ^
    問題出在哪里了?USER_ID域是一個(gè)空final(blank final),它是一個(gè)在聲明中沒有進(jìn)行初始化操作的final域[JLS 4.12.4]。很明顯,只有在對(duì)USER_ID賦值失敗時(shí),才會(huì)在try語句塊中拋出異常,因此,在catch語句塊中賦值是相當(dāng)安全的。不管怎樣執(zhí)行靜態(tài)初始化操作語句塊,只會(huì)對(duì)USER_ID賦值一次,這正是空final所要求的。為什么編譯器不知道這些呢?
    要確定一個(gè)程序是否可以不止一次地對(duì)一個(gè)空final進(jìn)行賦值是一個(gè)很困難的問題。事實(shí)上,這是不可能的。這等價(jià)于經(jīng)典的停機(jī)問題,它通常被認(rèn)為是不可能解決的[Turing 36]。為了能夠編寫出一個(gè)編譯器,語言規(guī)范在這一點(diǎn)上采用了保守的方式。在程序中,一個(gè)空final域只有在它是明確未賦過值的地方才可以被賦值。規(guī)范長篇大論,對(duì)此術(shù)語提供了一個(gè)準(zhǔn)確的但保守的定義[JLS 16]。因?yàn)樗潜J氐?,所以編譯器必須拒絕某些可以證明是安全的程序。這個(gè)謎題就展示了這樣的一個(gè)程序。
    幸運(yùn)的是,你不必為了編寫Java程序而去學(xué)習(xí)那些駭人的用于明確賦值的細(xì)節(jié)。通常明確賦值規(guī)則不會(huì)有任何妨礙。如果碰巧你編寫了一個(gè)真的可能會(huì)對(duì)一個(gè)空final賦值超過一次的程序,編譯器會(huì)幫你指出的。只有在極少的情況下,就像本謎題一樣,你才會(huì)編寫出一個(gè)安全的程序,但是它并不滿足規(guī)范的形式化要求。編譯器的抱怨就好像是你編寫了一個(gè)不安全的程序一樣,而且你必須修改你的程序以滿足它。
    解決這類問題的方式就是將這個(gè)煩人的域從空final類型改變?yōu)槠胀ǖ膄inal類型,用一個(gè)靜態(tài)域的初始化操作替換掉靜態(tài)的初始化語句塊。實(shí)現(xiàn)這一點(diǎn)的方式是重構(gòu)靜態(tài)語句塊中的代碼為一個(gè)助手方法:
    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;
     }
     }
     ...// The rest of the program is unchanged
    }
    程序的這個(gè)版本很顯然是正確的,而且比最初的版本根據(jù)可讀性,因?yàn)樗鼮榱擞蛑档挠?jì)算而增加了一個(gè)描述性的名字,而最初的版本只有一個(gè)匿名的靜態(tài)初始化操作語句塊。將這樣的修改作用于程序,它就可以如我們的期望來運(yùn)行了。
    總之,大多數(shù)程序員都不需要學(xué)習(xí)明確賦值規(guī)則的細(xì)節(jié)。該規(guī)則的作為通常都是正確的。如果你必須重構(gòu)一個(gè)程序,以消除由明確賦值規(guī)則所引發(fā)的錯(cuò)誤,那么你應(yīng)該考慮添加一個(gè)新方法。這樣做除了可以解決明確賦值問題,還可以使程序的可讀性提高。