不可變物件指實例化後外部可見狀態無法更改的物件。Java 類庫中 String、Integer、BigDecimal 即為例——代表物件生命週期內不可變更的單一值。
正確使用不可變類別能極大簡化程式設計:
HashMap/HashSet 鍵不可變物件值無變更風險,可自由快取引用,確信後續引用仍指向相同值。同理,可快取欄位與方法結果。可變物件則需謹慎:考慮排程器範例,Date 可變導致 scheduleTask 必須防禦性複製日期參數,否則兩任務可能同天執行,內部資料結構更可能損壞。此類錯誤極易疏忽、難捕捉、延遲顯現。不可變 Date 根本不可能發生此類錯誤。
多數執行緒安全問題源於多執行緒並發修改物件狀態(寫寫衝突)或一執行緒讀取時另一執行緒修改(讀寫衝突)。防止衝突需同步存取,確保物件不一致狀態時不可被存取——正確實作困難、需大量文件、可能損效能。正確建構的不可變物件(不讓 this 參考自建構式逸出)免除同步需求,因狀態不可變更,寫寫/讀寫衝突根本不存在。無需同步即可自由跨執行緒共享引用,極大簡化並發程式設計,減少潛在並發錯誤。
接收物件參數的方法不應修改物件狀態,除非文件明確允許或方法實際擁有物件。傳遞 java.awt.Point 給 Component.setLocation(),無法阻止方法修改傳入點位置、儲存引用稍後修改。雖 Component 不會這樣做,但非所有類都這麼「禮貌」。點狀態在不知情下改變,導致我們以為在原位置實則在別處——潛在危險。若 Point 不可變,惡意程式碼無法以此混亂危險方式修改程式狀態。
不可變物件產生最佳 HashMap/HashSet 鍵。可變物件可能根據狀態改變 hashCode()(如 StringHolder 範例)。用作 HashSet 鍵後修改狀態,會擾亂 HashSet:枚舉集合仍出現,但 contains() 查詢卻找不到。範例程式輸出 false、1、moo——極度混亂行為。
最適合表示抽象資料類型值:數字、列舉類型、顏色。Java 類庫基本數字類(Integer、Long、Float)、標準數字類型(BigInteger、BigDecimal)皆不可變。複數、任意精度有理數類別同樣適合。甚至包含多離散值的抽象類型(向量、矩陣)視應用需求也適合不可變實作。
java.awt.Color 為佳例:顏色在 RGB、HSB、CMYK 等表示法中為有序數值組,但視為顏色空間單一值而非獨立可尋址值組更有意義,故作不可變類別實作合理。
表示多基本值容器(點、向量、矩陣、RGB 顏色)用可變還是不可變?視使用方式而定:主要表示多維值(像素顏色)還是作為相關特性容器(視窗高寬)?特性修改頻率?修改時各組件值在應用中是否有獨立含義?
事件也是適合不可變實作的好例:生命週期短、常在建立執行緒外消費,不可變利大於弊。大多數 AWT 事件類非嚴格不可變但可微調。使用訊息傳遳通訊的系統,訊息物件設為不可變或許明智。
編寫不可變類別很容易,滿足以下條件即為不可變:
finalfinalthis 參考逸出Date 等可變類別)的欄位:
最後一組要求看似複雜,實則意味著:儲存陣列或可變物件引用時,必須確保類別對該物件擁有獨佔存取權(否則其他類可修改其狀態),且建構後不修改其狀態。此複雜度為允許不可變物件儲存陣列引用所必須——Java 無法強制禁止修改 final 陣列元素。註:若從建構式參數初始化陣列引用或其他可變欄位,必須防禦性複製呼叫者提供的參數或無法確保獨佔存取的資訊。否則呼叫者在建構式呼叫後修改陣列狀態。範例 3 展示正確與錯誤寫法:
class ImmutableArrayHolder {
private final int[] theArray;
// 正確:複製陣列
public ImmutableArrayHolder(int[] anArray) {
this.theArray = (int[]) anArray.clone();
}
// 錯誤:複製引用
public ImmutableArrayHolder(int[] anArray) {
this.theArray = anArray;
}
// 正確存取器:不暴露陣列引用
public int getArrayLength() { return theArray.length; }
public int getArray(int n) { return theArray[n]; }
// 正確存取器:使用 clone()
public int[] getArray() { return (int[]) theArray.clone(); }
// 錯誤存取器:暴露陣列引用
public int[] getArray() { return theArray; }
}
透過額外工作,可編寫使用非 final 欄位的不可變類別(如 String 標準實現用 hashCode 惰性計算),可能比嚴格 final 類別效能更好。若類別表示抽象類型值(數字、顏色),應實作 hashCode() 和 equals() 使物件在 HashMap/HashSet 中作鍵良好運作。禁止 this 從建構式逸出對維持執行緒安全至關重要。
部分資料項程式生命週期中恆常數,部分頻繁修改。常數資料顯然符合不可變性,複雜且頻繁變更狀態的物件通常不適合不可變實作。那「偶爾修改但不頻繁」的資料如何兼得不可變便利與執行緒安全?
util.concurrent 中的 CopyOnWriteArrayList 為佳例:最適合支援事件監聽器的類別(使用者介面組件)。監聽器列表可修改,但修改頻率遠低於事件產生頻率。
除修改列表時,CopyOnWriteArrayList 不修改底層陣列,而是創建新陣列丟棄舊陣列。行為類似 ArrayList。調用者取得迭代器(內部持有底層陣列引用)時,該陣列實際不可變,可無需同步或冒並發修改風險遍歷。消除遍歷前克隆列表或遍歷期間同步列表的需求——兩者麻煩、易錯、嚴重損效能。若遍歷遠多於插入/移除(常見),CopyOnWriteArrayList 提供更佳效能與便利存取。
使用不可變物件比可變物件容易得多:單一狀態、始終一致、內建執行緒安全、自由共享。徹底消除許多易發難測程式錯誤:無法跨執行緒同步存取、儲存引用前未克隆陣列/物件。編寫類別時,問自己「能否有效實作為不可變類別」總是值得——答案常令人驚訝地是肯定的。