String類不可變以及不可變類總結
String類在java中是immutable(不可變),因為它被關鍵字final修飾。當String例項建立時就會被初始化,並且以後無法修改例項資訊。String類是工程師精心設計的藝術品。
1.String為什麼不可變?
要了解String類建立的例項為什麼不可變,首先要知道final關鍵字的作用:final的意思是“最終,最後”。final關鍵字可以修飾類、方法、欄位。修飾類時,這個類不可以被繼承;修飾方法時,這個方法就不可以被覆蓋(重寫),在JVM中也就只有一個版本的方法--實方法;修飾字段時,這個欄位就是一個常量。
檢視java.lang.String方法時,可以看到:
"The String class represents character strings"意思是String類表示字串;String類被關鍵字final修飾,可以說明它不可被繼承;從"private final char value[]"可知,String本質上是一個char陣列。/** * The String class represents character strings. All string literals in Java programs, such as "abc", * are implemented as instances of this class. */ public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ... }
String類的成員欄位value是一個char[]陣列,而且也是用final關鍵字修飾,被final關鍵字修飾的欄位在建立後其值是不可變的,但也只是value這個引用地址不可變,可是Array陣列的值卻是可變的,Array陣列資料結構如下圖所示:
從圖中可以看出,Array變數只是棧上(stack)的一個引用,陣列中的資料儲存在堆上(heap)。String類裡的value是用final修飾,只能說在棧上(stack)這個value的引用地址不可變,可沒說堆裡的Array本身資料不可變。看下面這個例子:
final int[] value = {1,2,3,4,5};
int otherValue = {6,7,8,9,10};
value = otherValue;//編譯報錯
value是被final關鍵字修飾的,編譯器不允許把value指向堆另外一個地址;但如果直接對陣列元素進行賦值,就允許;如下面這個例子:
final int[] value = {1,2,3,4,5};
value[0] = 0;
所以說String是不可變,在後面所有的String方法裡沒有去動Array中的元素,也沒有暴露內部成員欄位。private final char value[],private的私有訪問許可權的作用都比final大。所以String是不可變的關鍵都是在底層實現的,而不是一個簡單的final關鍵字。
2.String類例項不可變的記憶體結構圖
String類例項不可變很簡單,如下圖所示是String類例項不可變的記憶體結構圖:
有一個字串s的值為"abcd",它再次被賦值為"abcdef",不是在原堆的地址上修改資料,而是重新指向一個新的物件,新的地址。
3.字串常量池
字串常量池是方法區(Method Area)中一個特殊的儲存區域。當一個字串被建立時,如果這個字串的值已經存在String pool中,就返回這個已經存在的字串引用,而不是建立一個新的物件。下面的程式碼只會在堆中建立一個物件:
String name="abcd";
String userName="abcd";
這樣在大量使用字串的情況下,可以節省記憶體空間,提高效率。但之所以能實現這個特性,String的不可變性是最基礎的一個必要條件。
4.String類不可變有什麼好處?
最簡單的就是為了安全和效率。從安全上講,因為不可變的物件不能被改變,他們可以在多個執行緒之間進行自由共享,這消除了進行同步的要求;從效率上講,設計成final,JVM才不用對相關方法在虛擬函式表中查詢,而是直接定位到String類的相關方法上,提高執行效率;總之,由於效率和安全問題,String被設計成不可變的,這也是一般情況下,不可變的類是首選的原因。
5.不可變類
不可變類只是它的例項不能被修改的類。每個例項中包含的所有資訊都必須在建立該例項時就提供,並在物件 的整個生命週期內固定不變。String、基本型別的包裝類、BigInteger和BigDecimal就是不可變得類。
為了使類成為不可變,必須遵循以下5條規則:①不要提供任何會修改物件狀態的方法。②保證類不會被擴充套件。③使所有的域都是final。④使所有的域都成為私有的。⑤確保 對於任何可變元件的互斥訪問。如果類具有指向可變物件的域,則必須確保該類的客戶端無法獲得指向這些物件的引用。
6.不可變類的優點和缺點
不可變類例項不可變性,具有很多優點。①不可變類物件比較簡單。不可變物件可以只有一種狀態,即被建立時的狀態。②不可變物件本質上是執行緒安全的,它們不要求同步。當多個執行緒併發訪問這樣的物件時,它們不會遭到破壞。實際上,沒有任何執行緒會注意到其他執行緒對於不可變物件的影響。所以,不可變物件可以被自由地分配。“不可變物件可以被自由地分配”導致的結果是:永遠不需要進行保護性拷貝。③不僅可以共享不可變物件,甚至也可以共享它們的內部資訊。④不可變物件為其他物件提供了大量的構件。如果知道一個複雜物件內部的元件不會改變,要維護它的不變性約束是比較容易的。
不可變類真正唯一的缺點是,對於每個不同的值都需要一個單獨的物件。建立這種物件的代價很高。
7.如何構建不可變類?
構建不可變類有兩種方式:用關鍵字final修飾類和讓類的所有構造器都變成私有的或者包級私有的,並新增公有的靜態工廠來替代公有的構造器。
為了具體說明用靜態工廠方法來替代公有的構造器,下面以Complex為例:
//複數類
public class Complex{
//實數部分
private final double re;
//虛數部分
private final double im;
//私有構造器
private Complex(double re,double im){
this.re = re;
this.im = im;
}
//靜態工廠方法,返回物件唯一例項
public static Complex valueOf(double re,double im){
return new Complex(re,im);
}
...
}
不可變的類提供一些靜態工廠方法,它們把頻繁請求的例項快取起來,從而當現在例項符合請求的時候,就不必建立新的例項。使用這樣的靜態工廠方法使得客戶端之間可以共享現有的例項,而不是建立新的例項,從而減低記憶體佔用和垃圾回收的成本。
總之,使類的可變性最小化。不要為每個get方法編寫一個相對應的set方法,除非有很好的理由要讓類成為可變的類,否則就應該是不可變的。如果有些類不能被做成是不可變的,仍然應該儘可能地限制它的可變性。不可變的類有很多優點,但唯一的缺點就是在特定的情況下存在潛在的效能問題。
PS:靜態工廠方法是什麼?
靜態工廠方法只是一個返回類的例項的靜態方法,如下面是一個Boolean的簡單例項。這個方法將boolean基本型別值轉換成一個Boolean物件引用。
public static Boolean valueOf(boolean b){
return b?Boolean.TRUE?Boolean.FALSE;
}
靜態工廠方法相對於構造器來說,具有很多優勢:
①建立的方法有名字;
②不必在每次呼叫它們的時候都建立一個新的物件;
③可以返回原返回型別的任何子類的物件。這樣我們在選擇返回物件的類時就有更大的靈活性,這種靈活性的一種應用是API可以返回物件,同時又不會使物件的類變成公有的。以這種方式隱藏實現類會使API變得非常簡潔,這項技術適用於基於介面的框架。
④在建立引數化型別例項時,它們使程式碼變得更加簡潔。編譯器可以替你找到型別引數,這被稱為型別推導。如下面這個例子
public static<k,v> HashMap<k,v> newInstance(){
return new HashMap<k,v>();
}
靜態工廠方法也有缺點:
①類如果不含公有的或者受保護的構造器,就不能被子類化。對於公有的靜態工廠方法所返回的非公有類也同樣如此。
②它們與靜態方法實際上沒有什麼區別。
簡而言之,靜態工廠方法和公有構造器都各有用處,我們需要理解它們各自的長處。結合實際情況,再做選擇。