Effective Java Item50 - 必要時進行保護型拷貝
September 26, 2017這篇是Effective Java - Make defensive copies when needed章節的讀書筆記
Item50: 必要時進行保護型拷貝
JAVA相對於C/C++來說 已經是個很安全的語言 你可以說C基本上就是把所有memory當作一個巨型Array 要非常小心處理memory的問題 但在java你要處理的問題已經少很多了 比如說buffer overflow, array overflow, wild pointer等等 但我們還是得永遠把client想成無惡不赦的壞蛋
來個例子
簡單易懂 我希望Period不能被改變 所以Class本身是final 所有variable是final 而且只有getter沒有setter 這樣不管client多邪惡都不能做壞事了吧?
上面的class簡單易破 只要這樣
崩潰 因為Date這個field本身是mutable 所以其實任何人都可以改變Date的instance
等等 final不是代表他不可變嗎 為什麼可以改
final代表的是start這個reference只能指到這個傢伙 不能指到其他人 那這個傢伙改變容貌了你也無可奈何
那怎麼辦呢 簡單 改constructor
這跟剛剛的差在哪裡呢 這就是本篇的主題 Defensive copy
他並不是直接把input argument 直接assign給instance variable 而是去call start.getTime之後 再丟進Date的constructor
這樣的話剛剛的
就沒有用了 因為這個end跟Period裡的end不是同一個
注意 Defensive copy發生在確認input的合理性之前 而且是針對被copy後的對象檢查而不是原本對象
為什麼要多此一舉呢 要是copy完才發現input不合理 不是很浪費時間嗎
原因很簡單 在multithread的程式中 很常出錯的地方就是確認input沒問題 到copy參數的過程中(這段期間稱為window of vulnerability) 在這段時間內class的state被其他thread改變了是很常見的race condition 所以先copy再檢查是比較好的做法
但其實改變constructor只解決了一半的問題
因為Date這個class是mutable 所以如果你直接回傳你的instance variable出去 別人還是可以做壞事
要防禦這種攻擊 要修改我們的兩個getter
一樣 Denfensive copy
做完這些防護措施後 無論使用者多麼卑劣 都不可能違反Period的end一定在start之後的保證
因為除了Period之外 沒有其他人可以碰到這兩個instance variable 真正達到了private的封裝
為什麼這裡要這麼麻煩用getTime() 直接用start.clone()不就好了?
在這裡可以 但其他時候用clone來做defensive copy會有安全性的漏洞 因為Date有可能會被繼承 我們無法保證clone會真的回傳Date而不是Date的subclass 而subclass的行為難以控制 所以在實作defensive copy的時候不要用clone
總結
每當你要寫一個方法或是constructor 確認以下幾點
1.Client給你的input是不是會直接進入到內部的資料結構 如果是
2.看看他給的東西是不是Mutable 如果是
3.你的Class能否容忍這個資料結構被改變 如果不能
4.你能不能保證你的Client不會亂改你的東西 如果不能
那你就該對你的對象進行defensive copy