本文與個人博客 zhiheng.me 同步發布,標題: Java中的不可變類。
Java中的不可變類
不可變類(Immutable Objects):當類的實例一經創建,其內容便不可改變,即無法修改其成員變量。
可變類(Mutable Objects):類的實例創建后,可以修改其內容。
Java 中八個基本類型的包裝類和 String 類都屬於不可變類,而其他的大多數類都屬於可變類。
與引用不可變的區別
需要特別注意的是,不可變類的不可變是指該類的實例不可變而非指向該實例的引用的不可變。
String s = "abc"; System.out.println("s:" + s); // 輸出s:abc s = "xyz"; System.out.println("s:" + s); // 輸出s:xyz
以上代碼顯示,不可變類 String 貌似是可以改變值的,但實際上並不是。變量 s 只是一個指向 String 類的實例的引用,存儲的是實例對象在內存中的地址。代碼中第三行的 “改變” 實際上是新實例化了一個 String 對象,並將 s 的指向修改到新對象上,而原來的對象在內存中並未發生變化,只是少了一個指向它的引用,並且在未來被垃圾回收前它都將保持不變。
public class Immutable { public static void main(String[] args) { String str = new String("abc"); String str2 = str; System.out.println(str == str2); // true str2 = "cba"; System.out.println(str == str2); // false System.out.println(str == row(str)); // true System.out.println(str == other(str)); // false } static private String row(String s){ return s; } static private String other(String s){ s="xyz"; //此處形參 s 指向了新的String對象,引用的地址發生變化 return s; } }
如此我們看到,對於不可變類的對象,都是通過新創建一個對象並將引用指向新對象來實現 變化 的。
通常,使用關鍵字 final 修飾的字段初始化后是不可變的,而這種不可變就是指引用的不可變。具體就是該引用所指對象的內存地址是不可變的,但並非該對象不可變。如果該對象也不可變,那么該對象就是不可變類的一個實例。
public class Immutable { public static void main(String[] args) { Immutable immutable = new Immutable(); final Inner inner = immutable.new Inner(); inner.value = 123; // 實例可變 // 下面語句編譯錯誤,inner 是final的,無法讓它指向新的對象(改變指向地址) // inner = it.new Inner(); Inner inner2 = inner; // 復制了一份引用,inner和inner2指向同一個對象 System.out.println(inner); // 將調用 toString 方法輸出對象內存地址 System.out.println(inner2); // inner和inner2具有相同的地址 System.out.println(inner.value); // 輸出 123 System.out.println(inner2.value); // 輸出123 inner2.value = 321; System.out.println(inner); // 輸出321 } class Inner{ private int value; } }
不可變類是如何實現的
immutable對象的狀態在創建之后就不能發生改變,任何對它的改變都應該產生一個新的對象。
因此,一個不可變類的定義應當具備以下特征:
- 所有成員都是 private final 的
- 不提供對成員的改變方法,例如:setXXXX
- 確保所有的方法不會被重載。手段有兩種:使用final Class(強不可變類),或者將所有類方法加上final(弱不可變類)。
- 如果某一個類成員不是基本類型(primitive type)或不可變類,必須通過在成員初始化(in)或者getter方法(out)時通過深度拷貝(即復制一個該類的新實例而非引用)方法,來確保類的不可變。
- 如果有必要,重寫hashCode和equals方法,同時應保證兩個用equals方法判斷為相等的對象,其hashCode也應相等。
下面是一個示例:
public final class ImmutableDemo { private final int[] myArray; public ImmutableDemo(int[] array) { // this.myArray = array; // 錯誤! this.myArray = array.clone(); // 正確 } public int[] get(){ return myArray.clone(); } }
上例中錯誤的方法不能保證不可變性,myArray 和形參 array 指向同一塊內存地址,用戶可以在 ImmutableDemo 實例之外通過修改 array 對象的值來改變實例內部 myArray 的值。正確的做法是通過深拷貝將 array 的值傳遞給 myArray 。同樣, getter 方法中不能直接返回對象本身,而應該是克隆對象並返回對象的拷貝,這種做法避免了對象外泄,防止通過 getter 獲得內部可變成員對象后對成員變量直接操作,導致成員變量發生改變。
對於不可變類,String 是一個典型例子,看看它的源碼也有助於我們設計不可變類。
不可變類的優點
不可變類有兩個主要有點,效率和安全。
- 效率
當一個對象是不可變的,那么需要拷貝這個對象的內容時,就不用復制它的本身而只是復制它的地址,復制地址(通常一個指針的大小)只需要很小的內存空間,具有非常高的效率。同時,對於引用該對象的其他變量也不會造成影響。
此外,不變性保證了hashCode 的唯一性,因此可以放心地進行緩存而不必每次重新計算新的哈希碼。而哈希碼被頻繁地使用, 比如在hashMap 等容器中。將hashCode 緩存可以提高以不變類實例為key的容器的性能。
-
線程安全
在多線程情況下,一個可變對象的值很可能被其他進程改變,這樣會造成不可預期的結果,而使用不可變對象就可以避免這種情況同時省去了同步加鎖等過程,因此不可變類是線程安全的。
當然,不可變類也有缺點:不可變類的每一次“改變”都會產生新的對象,因此在使用中不可避免的會產生很多垃圾。