[轉]爲何Java中的String不可變

筆主前言java

衆所周知,String是Java的JDK中最重要的基礎類之一,在筆主心中的地位已經等同於int、boolean等基礎數據類型,是超越了通常Object引用類型的高端大氣上檔次的存在。算法

可是稍有研究的人就會發現,String對象是不可修改的,源代碼中的String類被定義爲final,即爲終態,不可繼承,String也不提供任何直接修改對象內部值的方法,每次使用replace、substring、trim等方法,或是使用字符串鏈接符+時,都是返回一個全新的String對象,整個String對象的值只能經過構造函數,在初始化對象實例時一次性輸入(固然Java語法容許直接使用雙引號方式快捷獲取String對象實例)。數組

若是須要動態修改、構造字符串,則須要經過StringBuilder或StringBuffer對象進行操做,並在最終輸出時經過toString()、substring()等方法獲得String對象。直接使用String對象進行鏈接、增刪替換字符等操做,將不可避免地產生大量臨時String對象,影響CPU效率和增長資源回收負擔。緩存

 

今天偶然看到一個外文文章,較爲完整詳細客觀科學的論述了String類被如此設計成不可變結構的緣由,下面筆主結合本身的理解,儘可能經過淺顯的語言意譯成中文,科普一下知識。安全

原文連接:http://www.programcreek.com/2013/04/why-string-is-immutable-in-java/網絡


 

爲何Java中的String是不可變的?

 

要解釋String被設計成不可變結構的緣由,須要從存儲空間、同步性、數據類型等方面去分析。數據結構

 

解釋1:知足 String Pool (String intern pool) 字符串保留池的須要

Java語法設計中專門針對String類型,提供了一個特殊的存儲機制,叫字符串保留池String intern pool,簡單點說,這個池是在內存堆中專門劃分一塊空間,用來保存全部String對象數據,當程序猿構造一個新字符串String對象時,Java編譯機制會優先在這個池子裏查找是否已經存在能知足須要的String對象,若是有的話就直接返回該對象的地址引用(沒有的話就正常的構造一個新對象,丟進去存起來),所以實際上構造兩三個乃至成千上萬個同一句話的String對象,獲得的是同一個對象引用,這能避免不少沒必要要的空間開銷。多線程

然而若是String對象自己容許被二次修改值內容的話,其中一個引用對String對象的修改將不可避免地影響其餘正在引用該對象的變量,誘發出不可預測的後果。ide

 

* 上面所說的機制,僅適用於使用如下語法構造String對象的場景:函數

String string1 = "abcd";
String string2 = "abcd";  // string1 == string2String string1 = new String("abcd");
String string2 = new String("abcd");  // string1 != string2

 

解釋2:緩存Hashcode的須要

在HashMap等須要使用hashCode做爲鍵值存儲地址的數據結構中,String對象經常做爲這些數據結構的key值,常見地組合成如 HashMap<String, Object> 等類型的哈希表結構使用。

當HashMap須要隨機調取某個元素的時候(例如 hashMap.get("money"); ),HashMap將調用做爲key值的String對象的hashCode()方法,獲取能表明這個對象的惟一數值hashCode,定位這個鍵值對的實際存儲地址,繼而能夠像數組同樣經過array[index]這樣的下標方式直接訪問到目標元素。

因爲String類型具有不可變的特性,所以在String對象內的hashCode()方法實際上只需執行一次計算過程,計算後把結果緩存到一個內部私有變量 int hash 中,然後每次須要調用這個String對象的hashCode()方法時,僅僅須要把上次的計算結果hash返回去便可,在物理上強效地保證了這個結果的絕對正確性,當HashMap須要頻繁的讀取訪問任意一組鍵值對的時候,能節省很是多的CPU計算開銷。

 

解釋3:協助其餘對象的使用

* 這一部分看得不是很懂,就筆主的淺顯理解不能十分認同原文中的這個解釋理由,這一塊將按原文翻譯與筆主的理解觀點同步展現說明。

 

先展現一段不太真實的代碼:

複製代碼

HashSet<String> set = new HashSet<String>();
set.add(new String("a"));
set.add(new String("b"));
set.add(new String("c")); 
for(String a: set)
    a.value = "a";

複製代碼

原文:在這個例子中,若是String是可變的,那麼當它的值發生改變時將違反Set的設計(Set只能存儲相互惟一的元素)。這個代碼案例僅爲簡單的目的而設計,實際上String類不存在value變量。

 

按筆主理解解釋

在這段代碼中,三個String對象依次使用HashSet的add()方法合法地添加到了set對象中。

此時若是String是內容可變的話,那麼經過後面的for循環中 a.value = "a"; 這一句僞代碼,set對象中的三個成員變量都將變成String("a"),依據解釋1所提到的String Pool的狀況,這三個對象有可能會變成指向同一個字符串String對象"a",即set內部存了三個相同的對象,而這種狀況違背了Set類型的元素惟一性設計定義——Set中存儲的對象必需相互獨立惟一,不能重複。

退一步講,即便這三個對象依然是三個相互獨立的String對象"a",而根據String類設計的hashCode()算法,這三個獨立的String對象依然計算出了相同的hashCode值,顯然也是違反了HashSet的設計——三個對象同時指向了相同的存儲地址。

任何一種狀況都將在Set對象的外部不可控地違反了Set或HashSet自己設計的規定,誘發出不可預測的後果,而在語法檢測上卻毫無問題地經過了。

 

* 之因此說 a.value = "a"; 是僞代碼,並不是由於原文所說String類不存在value變量,而是由於String類內的私有變量 private final char value[] 是不容許外部操做的,另外在數組語法上也不容許按這種方式賦值,String中的value數組確實存儲的就是初始化傳入的字符串各字符數據,同時也是String計算hashCode算法的惟一依據。

** 若是嚴格定義String是內容可變這一前提,那麼解釋1中提出的String Pool將沒法實現,也就是說調用三次構造函數,必然返回三個互相獨立的對象,所以此例嚴格說並不會違反Set的元素惟一性定義,但依然會出現後面的相同hashCode值問題。

*** 通常而言,不一樣的對象經過hashCode()方法將獲得不一樣且惟一的hashCode值,但因爲這裏假想的可變String內容被更換,而致使不一樣的String對象產生相同hashCode值,在HashSet/HashMap中將產生異常表現,在本文最後的補充環節,筆主將使用一小段代碼進行模擬演示。

 

解釋4:安全性

String被普遍用於網絡鏈接、文件IO等多種Java基礎類的參數中,若是String內容可變的話,將潛在地帶來多種嚴重安全隱患,例如連接地址被暗中更改等,出於一樣的緣由,在Java反射機制中可變String參數也會致使潛在的安全威脅。

例如如下網絡鏈接代碼示例(修改自原文):

複製代碼

boolean connect(string url){    // 驗證url地址是否安全,不安全的網絡訪問將被異常拋出阻止
    if (!isSecure(url)) { 
        throw new SecurityException(); 
    }    // 上一步url已經過安全檢驗,但若是url在這裏可以被(其餘線程)其餘引用修改,將觸發嚴重的安全威脅    mayCauseProblemWhileOpen(url);
}

複製代碼

 

解釋5:不可變對象在物理上絕對性的線程安全

因爲不可變對象內容不可能被修改,所以能在多線程中被任意自由訪問而不會致使任何線程安全問題,同時也就不須要再作任何多餘的同步操做開銷。

 

總的來講,String的不可變特性設計就是出於效率和安全性的考慮,這也是其餘類通常狀況下更傾向於被設計成不可變特性的緣由。

 


 

 

補充:當HashMap趕上可變對象併產生相同hashCode值...

在這裏,筆主主要想討論在解釋3的***中提到的可變String對象產生相同hashCode值在HashMap中的異常表現問題。

先說明一個前提,String類中的hashCode()方法通過改寫,與Object中的hashCode()不一樣,String中的hashCode()計算的惟一依據是String對象自己的字符串內容,若是存在兩個內容同爲"Monkey"的String對象,這兩個對象通過hashCode()計算出的hashCode值將徹底相同,被HashMap視爲同一個key對象。

 

可變String模擬類:

package test;

/**
 * 可變字符串模擬類
 * 
 * @since 2014-6-21 下午6:14:16
 * @author Wavky.Wand
 */
public class ModifiableString {
    private String mContent;

    public ModifiableString(String content) {
        mContent = content;
    }

    /**
     * 更變字符串內容
     * 
     * @param mContent
     *            the content to set
     */
    public void setContent(String mContent) {
        this.mContent = mContent;
    }

    @Override
    public int hashCode() {
        return mContent.hashCode();
    }

    /**
     * 依據Java類設計規則與HashMap需求,同時改寫equals()方法,當兩個對象hashCode相同時,equals()方法判斷兩個對象爲相同
     */
    @Override
    public boolean equals(Object obj) {
        return obj.hashCode() == mContent.hashCode();
    }
}

 

測試類:

package test;

import java.util.HashMap;

/**
 * 
 * @since 2014-2-2 下午4:16:12
 * @author Wavky.Wand
 */
public class Test {

    public static void main(String[] args) {
        ModifiableString m1 = new ModifiableString("123");
        ModifiableString m2 = new ModifiableString("456");
        ModifiableString m3 = new ModifiableString("789");

        // 輸出三個對象的hashCode
        out("m1 hashCode:" + m1.hashCode());    // 48690
        out("m2 hashCode:" + m2.hashCode());    // 51669
        out("m3 hashCode:" + m3.hashCode());    // 54648
        
        // 初始化HashMap
        HashMap<ModifiableString, String> map = new HashMap<ModifiableString, String>();
        map.put(m1, "A");
        map.put(m2, "B");
        map.put(m3, "C");

        // 輸出初始化完畢後的HashMap
        out("初始化完畢的HashMap");
        out("map size:" + map.size());         // 3
        out("map.get(m1):" + map.get(m1));     // A
        out("map.get(m2):" + map.get(m2));     // B
        out("map.get(m3):" + map.get(m3));     // C

        out("迭代輸出HashMap的全部key值");
        for (ModifiableString m : map.keySet()) {
            out(m);                // @c9d5 @be32 @d578
        }
        
        out("迭代輸出HashMap的全部key對應的value值");
        for (ModifiableString m : map.keySet()) {
            out(map.get(m));       // A B C 三個value值依次正常輸出
        }

        out("迭代輸出HashMap的全部value值");
        for (String s : map.values()) {
            out(s);                // A B C 三個value值依次正常輸出
        }
        
        // 更改m3的內容,與m1內容相同
        out("m3.setContent(123)");
        m3.setContent("123");

        // 輸出更改m3內容後的信息
        out("m1 hashCode:" + m1.hashCode());    // 48690
        out("m2 hashCode:" + m2.hashCode());    // 51669
        out("m3 hashCode:" + m3.hashCode());    // 48690
        out("m1==m3:" + (m1 == m3));            // false
        out("m1.equals(m3):" + m1.equals(m3));  // true
        out("重設m3內容後的HashMap");
        out("map size:" + map.size());        // 3
        out("map.get(m1):" + map.get(m1));    // A
        out("map.get(m2):" + map.get(m2));    // B
        // 由於m3內容與m1一致,hashCode與equal方法判斷m3與m1相等,所以HashMap返回m1的內容,
        // 而m3對應的value值C沒法再經過key獲取,相似於內存泄露狀態
        out("map.get(m3):" + map.get(m3));    // A

        out("迭代輸出HashMap的全部key值");
        for (ModifiableString m : map.keySet()) {
            out(m);                // @be32 @c9d5 @be32 實際爲16進制無符號hashCode值,第一個與第三個相同
        }
        
        out("迭代輸出HashMap的全部key對應的value值");
        for (ModifiableString m : map.keySet()) {
            out(map.get(m));       // A B A 沒法經過任何一個key獲取到第三個value值C
        }

        out("迭代輸出HashMap的全部value值");
        for (String s : map.values()) {
            out(s);                // A B C 三個value值依次正常輸出
        }

        // 移除HashMap中的m3鍵值對
        out("map.remove(m3)");
        map.remove(m3);

        // 輸出更改m3內容後的信息
        out("刪除m3內容後的HashMap");
        out("map size:" + map.size());        // 2 HashMap內剩餘兩條鍵值對
        out("map.get(m1):" + map.get(m1));    // null 經過m1獲取value,無返回
        out("map.get(m2):" + map.get(m2));    // B
        out("map.get(m3):" + map.get(m3));    // null 經過m3獲取value,無返回

        out("迭代輸出HashMap的全部key值");
        for (ModifiableString m : map.keySet()) {
            out(m);                // @c9d5 @be32 m1或m3其中一個key被移除
        }
        
        out("迭代輸出HashMap的全部key對應的value值");
        for (ModifiableString m : map.keySet()) {
            out(map.get(m));       // B null 沒法經過任何一個key獲取到原第三個value值C
        }

        out("迭代輸出HashMap的全部value值");
        for (String s : map.values()) {
            out(s);                // B C 顯示實際上第一個鍵值對被刪除,而最後一個未被刪除,但沒法獲取到
        }
    }

    static void out(Object o) {
        System.out.println(o);
    }

}

 

分析

在這個略顯冗長的測試類中,分別執行了三個主要步驟:

一、使用三個獨立的ModifiableString對象(分別爲m1:123->A, m2:456->B, m3:789->C)初始化一個HashMap表對象,第一輪的HashMap信息輸出顯示,三個對象均被正常添加到map表中,並能分別經過三個key(m1/m2/m3)讀取對應的value值(A/B/C)。

二、更改第三個key對象m3的內容爲123(與第一個key對象m1相同),輸出信息顯示三個key依然爲相互獨立的對象,但m3的hashCode值變成與m1的同樣,第二輪HashMap信息輸出顯示,HashMap依然持有三個鍵值對,經過m3做爲key獲取到的value值爲m1對應的value值A,而m3原本對應的value值C卻沒法再經過keySet()方法返回的任何一個key獲取獲得。

三、刪除第三個key對象m3,第三輪HashMap信息輸出顯示,HashMap持有的鍵值對剩下兩個,分別是m2的key和m1或m3的key(因爲hashCode同樣,沒法區別),其中只有m2對應的value B能正常獲取到,而經過迭代value顯示出HashMap內被刪除的是第一個key對象m1對應的鍵值對,而非m3對應的,但m3對應的value值C依舊沒法經過keySet()方法返回的任何一個key獲取獲得。

 

 

經過三個步驟的HashMap內部結構解析圖能夠看到,HashMap中的每一個鍵值對依然持有各自獨立的key對象,可是在後面的兩步驟中,第三個鍵值對一直處於異常狀態,沒法正常的經過key對象獲取。

在深刻HashMap的源代碼中,逐步跟蹤put(K key, V value)->addEntry(int hash, K key, V value, int bucketIndex)->createEntry(int hash, K key, V value, int bucketIndex)方法能夠發現,在數據初始化過程當中,三個對象經過HashMap的put()方法,最終被存放在一個內部鍵值數組Entry<K,V>[] table中,存放的位置正好是這個對象的hashCode值表明的下標位置,一樣跟蹤get(Object key)->getEntry(Object key)方法能夠發現,使用key對象經過get()方法,最終獲取到的是HashMap中的table數組中,這個key的hashCode值表明的下標位置存儲的value值。

而上面的第二步驟經過人爲方式強制改變了第三個key對象m3的hashCode值,天然就丟失了獲取m3對應的value的索引了,由於整個數據更改過程並無通知到HashMap更新本來m3對應的value在table數組中的存儲位置,因此實際上從第二步驟開始,整個HashMap的內部數據就已經處於一種非同步的異常狀態,沒法繼續正常工做了。

 

結論

從這個實驗中能夠看出,HashMap並不支持key對象的hashCode發生動態變化,不可變對象是做爲HashMap的key的最優選擇。

另外也從側面反映出了String的不可變特性在解釋2解釋3中發揮出的重要做用。

相關文章
相關標籤/搜索