首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 理論與實踐:不可變物件的優勢與設計準則

     編輯:Java 相關知識

不可變物件指實例化後外部可見狀態無法更改的物件。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 事件類非嚴格不可變但可微調。使用訊息傳遳通訊的系統,訊息物件設為不可變或許明智。

編寫不可變類別的準則

編寫不可變類別很容易,滿足以下條件即為不可變:

  1. 所有欄位皆為 final
  2. 類別宣告為 final
  3. 建構期間不允許 this 參考逸出
  4. 包含可變物件引用(陣列、集合、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 從建構式逸出對維持執行緒安全至關重要。

偶爾修改的資料:CopyOnWriteArrayList

部分資料項程式生命週期中恆常數,部分頻繁修改。常數資料顯然符合不可變性,複雜且頻繁變更狀態的物件通常不適合不可變實作。那「偶爾修改但不頻繁」的資料如何兼得不可變便利與執行緒安全?

util.concurrent 中的 CopyOnWriteArrayList 為佳例:最適合支援事件監聽器的類別(使用者介面組件)。監聽器列表可修改,但修改頻率遠低於事件產生頻率。

除修改列表時,CopyOnWriteArrayList 不修改底層陣列,而是創建新陣列丟棄舊陣列。行為類似 ArrayList。調用者取得迭代器(內部持有底層陣列引用)時,該陣列實際不可變,可無需同步或冒並發修改風險遍歷。消除遍歷前克隆列表或遍歷期間同步列表的需求——兩者麻煩、易錯、嚴重損效能。若遍歷遠多於插入/移除(常見),CopyOnWriteArrayList 提供更佳效能與便利存取。

結語

使用不可變物件比可變物件容易得多:單一狀態、始終一致、內建執行緒安全、自由共享。徹底消除許多易發難測程式錯誤:無法跨執行緒同步存取、儲存引用前未克隆陣列/物件。編寫類別時,問自己「能否有效實作為不可變類別」總是值得——答案常令人驚訝地是肯定的。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved