ITEM 89:使用枚舉而不是 readResolve

ITEM 89: FOR INSTANCE CONTROL, PREFER ENUM TYPES TOREADRESOLVE
??item 3 描述了單例模式古沥,并給出了下面的單例類示例。這個類限制對其構(gòu)造函數(shù)的訪問,以確保只創(chuàng)建一個實例:

public class Elvis {
  public static final Elvis INSTANCE = new Elvis(); 
  private Elvis() { ... }
  public void leaveTheBuilding() { ... } }

??如 item 3 所述恋谭,如果將實現(xiàn) Serializable 的單詞添加到它的聲明中扳缕,這個類將不再是一個單例。類使用默認的序列化形式還是自定義序列化形式(item 87)并不重要举畸,類是否提供顯式的 readObject 方法(item 88)也不重要查排。
??任何 readObject 方法,無論是顯式的還是默認的抄沮,都會返回一個新創(chuàng)建的實例跋核,它將不是在類初始化時創(chuàng)建的相同實例岖瑰。
??readResolve 特性允許您用另一個實例替換由 readObject [Serialization, 3.7]創(chuàng)建的實例。如果正在反序列化的對象的類使用適當?shù)穆暶鞫x了一個 readResolve 方法砂代,則在反序列化后在新創(chuàng)建的對象上調(diào)用該方法蹋订。然后,該方法返回的對象引用將代替新創(chuàng)建的對象返回刻伊。在此特性的大多數(shù)使用中露戒,不會保留對新創(chuàng)建對象的引用,因此它立即符合垃圾收集的條件捶箱。
如果 Elvis 類被用來實現(xiàn) Serializable智什,下面的讀取-解析方法足以保證單例屬性:

// readResolve for instance control - you can do better!
private Object readResolve() {
  // Return the one true Elvis and let the garbage collector 
  // take care of the Elvis impersonator.
  return INSTANCE;
}

??此方法忽略反序列化的對象,返回在初始化類時創(chuàng)建的 Elvis 實例丁屎。因此荠锭,Elvis 實例的序列化形式不需要包含任何實際數(shù)據(jù);所有實例字段都應該聲明為 transient晨川。實際上证九,如果您依賴于 readResolve 進行實例控制,那么具有對象引用類型的所有實例字段都必須聲明為 transient共虑。否則愧怜,一個確定的攻擊者可能會在反序列化對象的readResolve 方法運行之前保護對其的引用,使用的技術(shù)有點類似于item 88 中的MutablePeriod 攻擊妈拌。
??這種攻擊有點復雜拥坛,但基本原理很簡單。如果一個單例包含一個非瞬態(tài)對象引用字段供炎,該字段的內(nèi)容將在該單例的 readResolve 方法運行之前被反序列化渴逻。這允許一個精心設(shè)計的流在對象引用字段的內(nèi)容被反序列化時“偷取”一個對最初反序列化的單例的引用。
??下面是它的工作原理音诫。首先惨奕,編寫一個 “stealer” 類,該類擁有一個 readResolve 方法和一個實例字段竭钝,該實例字段引用了被 stealer “隱藏”的序列化單例梨撞。在序列化流中,用 stealer 的實例替換 singleton 的非瞬態(tài)字段∠愎蓿現(xiàn)在就有了一個循環(huán):單元素包含偷取者卧波,偷取者引用單元素。
??因為單例包含竊取器庇茫,所以竊取器的 readResolve 方法在反序列化單例時首先運行港粱。因此,當 stealer 的 readResolve 方法運行時,它的實例字段仍然引用部分反序列化(還沒有解析)的單例查坪。
??stealer 的 readResolve 方法將引用從它的實例字段復制到一個靜態(tài)字段中寸宏,以便在readResolve 方法運行后可以訪問該引用。然后偿曙,該方法為其隱藏的字段返回正確類型的值氮凝。如果不這樣做,那么當序列化系統(tǒng)試圖將 stealer 引用存儲到這個字段時望忆,VM 將拋出 ClassCastException罩阵。
??為了使其具體化,考慮以下破碎單例:

// Broken singleton - has nontransient object reference field!
public class Elvis implements Serializable {
  public static final Elvis INSTANCE = new Elvis(); 
  private Elvis() { }
  private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
  public void printFavorites() { 
    System.out.println(Arrays.toString(favoriteSongs));
  }
  private Object readResolve() { 
    return INSTANCE;
  } 
}

??下面是一個“stealer”启摄,按照上面的描述構(gòu)造:

public class ElvisStealer implements Serializable { 
  static Elvis impersonator;
  private Elvis payload;
  private Object readResolve() {
    // Save a reference to the "unresolved" Elvis instance
    impersonator = payload;
    // Return object of correct type for favoriteSongs field
    return new String[] { "A Fool Such as I" }; 
  }
  private static final long serialVersionUID = 0; 
}

??最后稿壁,這里有一個丑陋的程序,它反序列化一個手工生成的流鞋仍,以生成有缺陷的單例的兩個不同實例常摧。反序列化方法從這個程序中被省略了搅吁,因為它與第354頁上的方法相同:

public class ElvisImpersonator {
  // Byte stream couldn't have come from a real Elvis instance!
  private static final byte[] serializedForm = {
(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05, 0x45, 0x6c, 0x76, 0x69, 0x73, (byte)0x84, (byte)0xe6, (byte)0x93, 0x33, (byte)0xc3, (byte)0xf4, (byte)0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53, 0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, 0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02};
  public static void main(String[] args) {
    // Initializes ElvisStealer.impersonator and returns // the real Elvis (which is Elvis.INSTANCE)
    Elvis elvis = (Elvis) deserialize(serializedForm); 
    Elvis impersonator = ElvisStealer.impersonator;
    elvis.printFavorites();
    impersonator.printFavorites(); 
  }
}

??運行這個程序會產(chǎn)生以下輸出威创,最終證明有可能創(chuàng)建兩個不同的 Elvis 實例(具有不同的音樂品味):

[Hound Dog, Heartbreak Hotel] 
[A Fool Such as I]

??你可以解決這個問題通過聲明 favoriteSongs 為 transient, 但你最好解決它通過使 Elvis 成為一個蛋元素的 enum類型(item 3)。ElvisStealer 為代表的攻擊,使用readResolve 方法防止“臨時”反序列化實例被攻擊者訪問是脆弱的,需要非常小心谎懦。
??如果您將可序列化的實例控制類編寫為枚舉肚豺,Java 保證除了聲明的常量之外不存在任何實例,除非攻擊者濫用了可訪問的特權(quán)方法(如AccessibleObject.setAccessible)界拦。任何可以這樣做的攻擊者已經(jīng)有足夠的特權(quán)來執(zhí)行任意的本地代碼吸申,所有的賭注都是無效的。

// Enum singleton - the preferred approach
public enum Elvis {
  INSTANCE;
  private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" }; 
  public void printFavorites() {
    System.out.println(Arrays.toString(favoriteSongs)); 
  }
}

??對實例控件使用 readResolve 并沒有過時享甸。如果必須編寫一個可序列化的實例控制類截碴,其實例在編譯時是未知的,則不能將該類表示為 enum 類型蛉威。
??readResolve 的可訪問性很重要日丹。如果在 final 類上放置一個 readResolve 方法,那么它應該是私有的蚯嫌。如果將 readResolve 方法放在非 final 類上哲虾,則必須仔細考慮其可訪問性。如果它是私有的择示,它將不適用于任何子類束凑。如果它是包私有的,它將只應用于同一包中的子類栅盲。如果它是受保護的或公共的汪诉,它將應用于所有不覆蓋它的子類。如果 readResolve 方法是受保護的或公共的谈秫,而子類沒有覆蓋它扒寄,那么反序列化子類實例將產(chǎn)生超類實例拴签,這可能會導致 ClassCastException 異常。
??總之旗们,請盡可能使用枚舉類型來強制實例控制不變量蚓哩。如果這是不可能的,并且您需要一個既可序列化又受實例控制的類上渴,那么您必須提供一個 readResolve 方法岸梨,并確保類的所有實例字段都是原語或瞬態(tài)字段。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末稠氮,一起剝皮案震驚了整個濱河市曹阔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隔披,老刑警劉巖赃份,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異奢米,居然都是意外死亡抓韩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門鬓长,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谒拴,“玉大人,你說我怎么就攤上這事涉波∮⑸希” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵啤覆,是天一觀的道長苍日。 經(jīng)常有香客問我,道長窗声,這世上最難降的妖魔是什么相恃? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮嫌佑,結(jié)果婚禮上豆茫,老公的妹妹穿的比我還像新娘。我一直安慰自己屋摇,他們只是感情好揩魂,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著炮温,像睡著了一般火脉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天倦挂,我揣著相機與錄音畸颅,去河邊找鬼。 笑死方援,一個胖子當著我的面吹牛没炒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播犯戏,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼送火,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了先匪?” 一聲冷哼從身側(cè)響起种吸,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呀非,沒想到半個月后坚俗,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡岸裙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年猖败,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哥桥。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡辙浑,死狀恐怖激涤,靈堂內(nèi)的尸體忽然破棺而出拟糕,到底是詐尸還是另有隱情,我是刑警寧澤倦踢,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布送滞,位于F島的核電站,受9級特大地震影響辱挥,放射性物質(zhì)發(fā)生泄漏犁嗅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一晤碘、第九天 我趴在偏房一處隱蔽的房頂上張望褂微。 院中可真熱鬧,春花似錦园爷、人聲如沸宠蚂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽求厕。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呀癣,已是汗流浹背美浦。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留项栏,地道東北人浦辨。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像沼沈,于是被迫代替她去往敵國和親荤牍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345