衆所周知, String
是一個不可變的,由 final
修飾的類。那麼它的不可變性體如今哪裏呢? 看下面一段簡單的代碼:html
String str= "123";
str = "456";
複製代碼
相信應該沒人會以爲這段代碼是錯誤的,那麼這符合 String
的不可變性嗎?String
的不可變性是如何體現的? 不可變性的好處是什麼?帶着這些疑問,read the fuck source code !java
Effective Java
中第 15 條 使可變性最小化
中對 不可變類
的解釋:數組
不可變類只是其實例不能被修改的類。每一個實例中包含的全部信息都必須在建立該實例的時候就提供,而且在對象的整個生命週期內固定不變。爲了使類不可變,要遵循下面五條規則:緩存
1. 不要提供任何會修改對象狀態的方法。安全
2. 保證類不會被擴展。 通常的作法是讓這個類稱爲
final
的,防止子類化,破壞該類的不可變行爲。bash3. 使全部的域都是 final 的。微信
4. 使全部的域都成爲私有的。 防止客戶端得到訪問被域引用的可變對象的權限,並防止客戶端直接修改這些對象。函數
5. 確保對於任何可變性組件的互斥訪問。 若是類具備指向可變對象的域,則必須確保該類的客戶端沒法得到指向這些對象的引用。ui
在 Java 平臺類庫中,包含許多不可變類,例如 String , 基本類型的包裝類,BigInteger, BigDecimal 等等。綜上所述,不可變類具備一些顯著的通用特徵:類自己是 final 修飾的;全部的域幾乎都是私有 final
的;不會對外暴露能夠修改對象屬性的方法。經過查閱 String
的源碼,能夠清晰的看到這些特徵。this
回到開頭提出的問題,對於這段代碼:
String str= "123";
str = "456";
複製代碼
下面這張圖清楚地解釋了代碼的執行過程:
執行第一行代碼時,在堆上新建一個對象實例 123
,str
是一個指向該實例的引用,引用包含的僅僅只是實例在堆上的內存地址而已。執行第二行代碼時,僅僅只是改變了 str
這個引用的地址,指向了另外一個實例 456
。因此,正如前面所說過的,不可變類只是其實例不能被修改的類。 給 str
從新賦值僅僅只是改變了它的引用而已,並不會真正去改變它原本的內存地址上的值。這樣的好處也是顯而易見的,最簡單的當存在多個 String
的引用指向同一個內存地址時,改變其中一個引用的值並不會對其餘引用的值形成影響。固然還有其餘的一些好處,這個放在後面再總結。
那麼,String
是如何保持不可變性的呢?結合 Effective Java
中總結的五條原則,閱讀它的 源碼 以後就一清二楚了。
看一下 String
的幾個域:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
...
...
}
複製代碼
String
類是 final
修飾的,知足第二條原則:保證類不會被擴展。
分析一下它的幾個域:
private final char value[] :
能夠看到 Java 仍是使用字節數組來實現字符串的,而且用 final
修飾,保證其不可變性。這就是爲何 String 實例不可變的緣由。
private int hash :
String的哈希值緩存
private static final long serialVersionUID = -6849794470754667710L :
String對象的 serialVersionUID
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0] :
序列化時使用
其中最主要的域就是 value
,表明了 String對象的值。因爲使用了 private final
修飾,正常狀況下外界沒有辦法去修改它的值的。正如第三條 使全部的域都是 final 的。 和第四條 使全部的域都成爲私有的 所描述的。難道這樣一個 private
加上 final
就能夠保證萬無一失了嗎?看下面代碼示例:
final char[] value = {'a', 'b', 'c'};
value[2] = 'd';
複製代碼
這時候的 value
對象在內存中已是 a b d
了。其實 final
修飾的僅僅只是 value
這個引用,你沒法再將 value
指向其餘內存地址,例以下面這段代碼就是沒法經過編譯的:
final char[] value = {'a', 'b', 'c'};
value = {'a', 'b', 'c', 'd'};
複製代碼
因此僅僅經過一個 final
是沒法保證其值不變的,若是類自己提供方法修改實例值,那就沒有辦法保證不變性了。Effective Java 中的第一條原則 不要提供任何會修改對象狀態的方法 。String 類也很好的作到了這一點。在 String
中有許多對字符串進行操做的函數,例如 substring
concat
replace
replaceAll
等等,這些函數是否會修改類中的 value
域呢?咱們看一下 concat()
函數的內部實現:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
複製代碼
注意其中的每一步實現都不會對 value
產生任何影響。首先使用 Arrays.copyOf()
方法來得到 value
的拷貝,最後從新 new
一個String對象做爲返回值。其餘的方法和 contact
同樣,都採起相似的方法來保證不會對 value
形成變化。的的確確,String
類中並無提供任何能夠改變其值的方法。相比 final
而言,這更能保障 String 不可變。
關於 Java 內存區域的相關知識能夠閱讀 《深刻理解 Java 虛擬機》
一書中相關章節或者個人對應的 讀書筆記。
細心的讀者應該發現上面出現過兩種 String 對象的寫法:
String str1 = "123";
String str2 = new String("123");
System.out.println(str1 == str2);
複製代碼
結果顯然是 false
。咱們都知道字符串常量池的概念,JVM 爲了字符串的複用,減小字符串對象的重複建立,特別維護了一個字符串常量池。第一種字面量形式的寫法,會直接在字符串常量池中查找是否存在值 123
,若存在直接返回這個值的引用,若不存在建立一個值爲 123
的 String 對象並存入字符串常量池中。而使用 new
關鍵字,則會直接在堆上生成一個新的 String 對象,並不會理會常量池中是否有這個值。因此本質上 str1
和 str2
指向的內存地址是不同的。
那麼,使用 new
關鍵字生成的 String 對象能夠進入字符串常量池嗎?答案是確定的,String 類提供了一個 native 方法 intern()
用來將這個對象加入字符串常量池:
String str1 = "123";
String str2 = new String("123");
str2=str2.intern();
System.out.println(str1 == str2);
複製代碼
打印結果爲 true
。str2
調用 intern()
函數後,首先在字符串常量池中尋找是否存在值爲 123
的對象,若存在直接返回該對象的引用,若不存在,加入 str2
並返回。上述代碼中,常量池中已經存在值爲 123
的 str1
對象,則直接返回 str1
的引用地址,使得 str1
和 str2
指向同一個內存地址。
那麼說了半天的字符串常量池在內存中處於什麼位置呢?常量池是方法區的一部分,用於存放編譯期生成的各類字面量和符號引用,運行期間也有可能將新的常量放入池中。在 Java 虛擬機規範中把方法區描述爲堆的一個邏輯部分,但它卻有一個別名叫 Non-Heap
,目的應該是爲了和 Java 堆區分開。
Effective Java
中總結了不可變類的特色。
綜上所述,String
的確是個不可變類,可是真的沒有辦法改變 String 對象的值嗎?答案確定是否認的,反射機制能夠作到不少日常作不到的事情。
String str = "123";
System.out.println(str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = '3';
複製代碼
執行結果:
123
133
複製代碼
很顯然,經過反射能夠破壞 String
的不可變性。
除了 String 類,系統類庫中還提供了一些其餘的不可變類,基本類型的包裝類、BigInteger、BigDecimal等等。這些不可變類比可變類更加易於設計、實現和使用,不容易出錯且更加安全。另外,要記住並不只僅是靠一個 final
關鍵字來實現不可變的,更多的是靠類內部的具體實現細節。
文章同步更新於微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!