關(guān)于String的不可變性

0 概述

  • 眾所周知,java中String為不可變的,即一旦一個(gè)String對象在內(nèi)存中被創(chuàng)建出來就無法被修改。
    具體表現(xiàn)為,String類的所有方法都沒有改變字符串本身的值,而是返回了一個(gè)新的對象。
  • 比如以下代碼:
        String str = "a";
        str =  "b";
image.png

事實(shí)上,內(nèi)容為"a"的字符串對象并沒有改變,只是str指向的對象地址發(fā)生了改變。

1 String的不可變性是如何保證的

JDK源碼中,存放String內(nèi)容的實(shí)際上是char數(shù)組,這個(gè)數(shù)組是final類型的,一經(jīng)初始化就不能改變。

 /** The value is used for character storage. */
    private final char value[];

但是,char數(shù)組是引用類型,final類型只能保證value的地址不可變,不能保證value數(shù)組的內(nèi)容不能變化。即完全可以通過以下方式改變value的內(nèi)容:

final char[] value = {'a','b','c'};
value[0] = 'd';

所以,重點(diǎn)是字符數(shù)組為private訪問權(quán)限,所以只有SUN的工程師可以拿到這個(gè)字符數(shù)組。而SUN的工程師在所有方法中都沒有改變該字符數(shù)組的值。另外,String被定義為Final類型,也防止了通過繼承之后破壞不可變性。

2 String為什么設(shè)計(jì)為不可變?

Sring設(shè)計(jì)為不可變,顯然是優(yōu)點(diǎn)要大于缺點(diǎn)。優(yōu)點(diǎn)主要是效率高,安全性高。我們先看優(yōu)點(diǎn)有哪些:

2.1 優(yōu)點(diǎn)

1 效率

效率主要體現(xiàn)在,基于Sting不可變,可以緩存字符串以及Hashcode,節(jié)省時(shí)間和空間。

  • String intern pool(字符串保留池)
    基于Sting不可變,我們就可以用緩存池將String對象緩存起來,同時(shí)把一個(gè)String對象的地址賦值給多個(gè)String引用,這樣可以安全保證多個(gè)變量共享同一個(gè)對象。如果Java中的String對象可變的話,一個(gè)引用操作改變了對象的值,那么其他的變量也會受到影響。
    Java語法中專門給String類型設(shè)計(jì)有String intern pool,當(dāng)程序員構(gòu)造一個(gè)新的字符串時(shí)(只適用String str = "abc"的場景,不適用new String("abc")的場景),會優(yōu)先在池子中查找是否已經(jīng)存在內(nèi)容相同的String對象,如果有則直接返回該對象的地址引用,沒有就會構(gòu)造一個(gè)新對象,放進(jìn)池子,再返回地址引用。因此,構(gòu)造一萬個(gè)string x = "abc",實(shí)際上得到都是同一個(gè)引用,避免很多不必要的空間開銷。

  • 緩存Hashcode

String對象內(nèi)的hashCode()方法實(shí)際上只需執(zhí)行一次計(jì)算過程(惰性計(jì)算),計(jì)算后把結(jié)果緩存到一個(gè)內(nèi)部私有變量 int hash中,再次調(diào)用hashCode()方法時(shí)了,直接返回hash。例如,HashMap以Srting為key,需要頻繁讀取訪問任意鍵值對時(shí),能夠節(jié)省很多的cpu計(jì)算開銷。

2 安全性

  • 線程安全

不可變對象在物理上是絕對性的線程安全。由于不可變對象不可能被修改,因此能夠在多線程中被任意自由訪問而不導(dǎo)致線程安全問題,不需要多余的同步操作。即在并發(fā)場景下,多個(gè)線程同時(shí)讀一個(gè)資源,并不會引發(fā)競態(tài)條件,只有對資源進(jìn)行讀寫才有危險(xiǎn)。不可變對象不能被寫,所以線程安全。

  • 其它地方使用安全

String被廣泛用于網(wǎng)絡(luò)連接、文件IO等多種Java基礎(chǔ)類的參數(shù)中,如果String內(nèi)容可變的話,將潛在地帶來多種嚴(yán)重安全隱患,例如鏈接地址被暗中更改等。

下面是截取的一個(gè)示例,用不可變的String與可變的StringBuilder作為對比。


image.png

在例子中,客戶端可以拿到String參數(shù)s和StringBuilder參數(shù)sb,不能改變原始的s,卻能改變sb。如果String可變,不安全性就體現(xiàn)在這里。

2.1 缺點(diǎn)

1 喪失了部分靈活性

我們平時(shí)使用的大部分都是可變對象,如果內(nèi)容變化,只需要setValue()更新一下就可以了,不需要重新創(chuàng)建一個(gè)對象。當(dāng)然,我們完全可以使用StringBuilder來彌補(bǔ)這個(gè)缺點(diǎn)。

3 脆弱的不可變性

  • 任何東西都可以靠JNI改變。一旦有調(diào)用C語言,你將在很多方面打破很多事情我甚至無法數(shù)清…
  • 使用純java,通過反射改變String對象也很容易。
public class StringModifier {
    public static void main(String[] str){
        try {
            String test1="aaaa";
            String test2 =test1;
            String test3 = new String(test1);
            String test4 = new String(test1.toCharArray());

            Field values = String.class.getDeclaredField("value"); 
            values.setAccessible(true);
            char[] ref = (char [])values.get(test1);
            ref[0] = 'b';
 
            System.out.println("aaaa");
            System.out.println(test1+" "+test2+" "+test3+" "+test4);
        } catch (NoSuchFieldException|SecurityException|
            IllegalArgumentException|IllegalAccessException ex) {
        }
    }
}

代碼輸出可能出乎你的意料:


image.png

我們知道,test1、test2同一String對象的引用。test3雖然持有的引用和test1的String對象不一樣,但是value[]數(shù)組是同一個(gè)。而test4的初始化方式,會生成新的數(shù)組并且拷貝每個(gè)元素,并創(chuàng)建新的String對象,所以不受影響。而System.out.println("aaaa")最終會調(diào)用到println(String s),棧中的s和test1同樣指向同一地址。
詳情可以參考String類源碼中的初始化方式:http://www.itdecent.cn/p/e5461012dcea

3 總結(jié)

總的來說,String作為Java中使用最為廣泛的一個(gè)類,設(shè)計(jì)為不可變,是出于效率與安全性方面考慮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容