淺談不可變特性


不可變(Immutability)是函數式程式設計的基本特性之一,若你試著去瞭解函數式程式設計,會看到有不少說法是這麼描述:「純函數式語言中的變數是不可變的。」是這樣的嗎?基本上沒錯,在純函數式語言中(像是Haskell),當你說 x = 1,那麼無法再修改它的值,x 就是 11 的名稱(而不是變數)就是 x,不會再是其他的東西,實際上,在純函數式語言中,並沒有變數的概念存在。

Java 並不是以函數式作為主要典範,一開始沒有不可變的概念與特性,在 Java 中想使用變數來模仿不可變特性,通常會是把變數加上 final 修飾,然後試圖在這樣的限制之上,將程式目的實現出來。

談到不可變特性,就會相對應地談到副作用(Side effect),一個具有副作用的方法會改變物件狀態,例如 Collectionssort() 方法,會調整 List 實例的元素順序,這就使得 List 實例的狀態改變,因而 sort() 方法是有副作用的方法,而 List 本身的 add() 方法,會增加內含元素的數量,這就使得 List 實例的狀態改變,因而 List 本身的 add() 方法是有副作用的方法。

一個 Java 應用程式在運行期間,系統本身的狀態是在不斷變化的,而系統的狀態就是由許多物件的狀態來組成,如果程式語言本身有變數的概念,在撰寫時就容易調整變數值,從而容易調整物件狀態,也就容易變更整個系統狀態。

然而,副作用是個雙面刃,在一個設計不良的系統中,若沒有適當地控管副作用,追蹤物件的狀態就會越來越困難,最後系統的狀態就會難以掌握,常見的問題是除錯困難,難以追查系統發生錯誤的原因,更可怕的是,你明明知道程式應該是寫錯了,系統卻能正常運作,只能擔憂著哪天踏到裏頭的地雷而爆出系統大洞。

如果變數不可變,那設計出來的方法就不會有副作用,物件狀態也會是不可變,不可變物件(Immutable object)有許多好處,像是在並行(Concurrent)程式設計時,就不用擔心那些執行緒共用競爭的問題;在面對資料處理問題若需要一些 Collection 物件,像是有序的 List、收集不重複物件的 Set 等,如果這些物件不可變,那麼就有可能共用資料結構,達到節省時間及空間之目的。

Java 畢竟不是以函數式為主要典範,在設計 Collection 框架時,並沒有為不可變物件設計專用型態,看看 Collection 介面就知道了,那些 add()remove() 等方法就直接定義在上頭。有趣的是,在〈Collections Framework Overview〉中談到了,有些方法操作都是選用的(Optional),如果不打算提供實作的方法,可以丟出 UnsupportedOperationException,而實作物件必須在文件上指明,支援哪些操作。

雖然 Java 不是以函數式為主要典範,然而有時在設計上,為了限制副作用的發生,會希望某些物件具有不可變的特性,以便易於掌握物件狀態,從而使得系統的某個部份容易掌握其狀態。

由於 CollectionsMap 在程式中會用來收集與管理物件,為了容易掌握這些物件的狀態,在 JDK8 以及先前的版本上,會透過 Collections 上提供的 unmodifiableCollection()unmodifiableList()unmodifiableSet()unmodifiableMap()static 方法來取得一個無法修改的(unmodifiable)物件,然而這還不夠,為了更進一步支援不可變特性,JDK9 在 ListSetMap 上直接提供了 of() 方法,用以建立不可變的 ListSetMap 實作物件。

就實務面上,兩者都應該認識,而認識 CollectionsunmodifiableXXX() 方法,也會比較清楚瞭解到,為何 JDK9 在 ListSetMap 上要提供 of() 方法以建立不可變的 ListSetMap 實作物件。

在 JDK8 或舊版 JDK 上,可以透過 guava-libraries 來取得不可變的 ListSetMap 實作物件,你可以先參考〈Guava 教學〉中的介紹。