不可變(Immutability)是函數式程式設計的基本特性之一,若你試著去瞭解函數式程式設計,會看到有不少說法是這麼描述:「純函數式語言中的變數是不可變的。」是這樣的嗎?基本上沒錯,在純函數式語言中(像是Haskell),當你說 x = 1
,那麼無法再修改它的值,x
就是 1
,1
的名稱(而不是變數)就是 x
,不會再是其他的東西,實際上,在純函數式語言中,並沒有變數的概念存在。
Java 並不是以函數式作為主要典範,一開始沒有不可變的概念與特性,在 Java 中想使用變數來模仿不可變特性,通常會是把變數加上 final
修飾,然後試圖在這樣的限制之上,將程式目的實現出來。
談到不可變特性,就會相對應地談到副作用(Side effect),一個具有副作用的方法會改變物件狀態,例如 Collections
的 sort()
方法,會調整 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 不是以函數式為主要典範,然而有時在設計上,為了限制副作用的發生,會希望某些物件具有不可變的特性,以便易於掌握物件狀態,從而使得系統的某個部份容易掌握其狀態。
由於 Collections
與 Map
在程式中會用來收集與管理物件,為了容易掌握這些物件的狀態,在 JDK8 以及先前的版本上,會透過 Collections
上提供的 unmodifiableCollection()
、unmodifiableList()
、unmodifiableSet()
、unmodifiableMap()
等 static
方法來取得一個無法修改的(unmodifiable)物件,然而這還不夠,為了更進一步支援不可變特性,JDK9 在 List
、Set
、Map
上直接提供了 of()
方法,用以建立不可變的 List
、Set
或 Map
實作物件。
就實務面上,兩者都應該認識,而認識 Collections
的 unmodifiableXXX()
方法,也會比較清楚瞭解到,為何 JDK9 在 List
、Set
、Map
上要提供 of()
方法以建立不可變的 List
、Set
或 Map
實作物件。
在 JDK8 或舊版 JDK 上,可以透過 guava-libraries 來取得不可變的 List
、Set
或 Map
實作物件,你可以先參考〈Guava 教學〉中的介紹。