天天看點

一個小小的String問題引發的血案

今天在項目中遇到了一個問題,然後我頭鐵的認為一直是bug,結果居然是String引起的,我一直沒有往String這個點上去思考,直到debug之後… …

(菜是原罪呀😣)

不知道你對String了解多少呢?
一般來說經常用過String的人都會說String是不可變的,你覺得呢?
這個不可變到底該怎麼了解?是值?位址?還是其他不可變呢?
= "a";
        str = "b";
        System.out.println(str);      

看上面這一段代碼,你認為它的答案是什麼呢?

如果你認為是aaa,那就不好意思,錯了。

答案:b

如果讓很多人回答,會出現三種不同的意見

(第一種和第三種答案雖相同,但是思想不同)

第一種:b(這一種雖然答案是對的,但還不如答錯的人,因為我估計說這種答案的人沒看過String源碼

第二種:a(這一種答案雖然是錯的,但應該了解過String的源碼,知道String是不可變的

第三種:b(此b非彼b,這種是最正确的,至于為什麼,下面會說)

為什麼會有字元串常量池

因為字元串就是對象,我們都知道配置設定對象需要消耗高昂的時間和空間;另一方面,我們經常會使用字元串;是以為了減少記憶體開銷和提高性能,JVM處理字元串時必須要進行優化。

然後字元串常量池便出現了,當JVM運作時,系統會單獨配置設定一塊空間,這段空間也就是字元串常量池。

當我們建立字元串時,JVM會先檢查字元串常量池是否會有該字元串,如果有,那麼就直接傳回該字元串的引位址;如果沒有,則建立該字元串放到字元串常量池中并傳回位址。

為什麼不可變?

觀看String底層代碼可以看出由于數組使用了final關鍵字進行修飾,導緻該String類的不可變性。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];      

那麼由于String的不可變性,那麼常量池中肯定不會出現相同的字元串。

不可變的是什麼?

了解了字元串常量池這個應該就很好分析了。

String str = “a”;字元串常量池沒有相應的字元串,是以會在字元串常量池中建立該字元串,然後将位址傳回給str

str = “b”;這一步會發生什麼呢?由于String的不可變性,那麼a是不可能改成b的;是以此時是str這個位址發生了變化。首先會建立b這個字元串,然後将位址傳回給str,是以此時str指向了b;a還是a,還是那片空間,沒有任何變化。

簡單了說,變的隻是引用,和字元串本身的那片空間沒有任何半毛錢關系

從String中的方法探讨不可變性

字元串的截取,傳回一個新的對象
public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //不過開始索引不為0,傳回了一個新的String對象
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }      
字元串的截取,傳回一個新的String對象
public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //類似上面
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }      
字元串的拼接(拼接之後copy到一個新的String對象)
public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        //copy
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }      

這樣的方法還有很多很多,我們可以發現其中的特點,如果傳回值類型是String,那麼都是傳回了一個新的對象。另外如果傳回值是其他類型,那麼在該方法中使用該字元串,依舊是先進行複制到一個新的數組中;然後再對其操作。

為什麼要将String設定為不可變性

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
  
    private final char value[];      

從第一個類的修飾符final可以得出,該類是不可繼承的,也是為了防止其它人繼承該類之後,對其安全性造成破壞。

一般來說操作字元串有三個重要的類,分别是

String、StringBuffer、StringBuilder

差別就不再多說

舉個StringBuffer的例子

= new StringBuffer("aaa");
        StringBuffer stringBuffer2 = new StringBuffer("bbb");
        List<StringBuffer> list = new ArrayList<>();
        list.add(stringBuffer);
        list.add(stringBuffer2);
        StringBuffer stringBuffer3 = stringBuffer;
        stringBuffer3.append("bbb");
        System.out.println(list);      
[aaabbb, bbb]

通過上面這個例子可以看出,我建立一個list容器;然後向其中加入兩個StringBuffer對象,通過第三個StringBuffer引用改變了容器中的内容,這是不是破壞了安全性。

再看下面這個例子

String str = new String("aaa");
        String str2 = new String("bbb");
        List<String> list = new ArrayList<>();
        list.add(str);
        list.add(str2);
        String str3 = str;
        str3  += "bbb";
        System.out.println(list);