Java 中String 類的不可變性與字符串拼接解析

@TOC

一、String 類是不可變的

1.1 不可變的原因

在Java 中,對于String 類的定義如下:


在這里插入圖片描述

由圖可知,String 類的值存儲于其私有變量value 中,而變量 value 是final 修飾的。
而在Java 中,final 修飾引用變量時(shí)代表給該引用無法修改其對象但可以改變其狀態(tài)。
同時(shí)經(jīng)過閱讀源碼,我們發(fā)現(xiàn),在String 類中 value 是私有變量且沒有提供對應(yīng)的setter 方法;除了構(gòu)造方法外,類中的方法也不會觸碰value 中的元素。
以substring(int beginIndex,int endIndex) 方法為例,該方法并不會去修改當(dāng)前String 對象的任何變量,而是使用 new String(...) 直接創(chuàng)建一個(gè)新的String 對象或返回自身。
也就是說,對于String 對象,一旦我們對其進(jìn)行了賦值,該對象中的value 變量也就不再會有變化了。
這就是為什么我們說String 類是不可變類的原因。

1.2 不可變的好處

String 類是不可變類,其對象已經(jīng)創(chuàng)建就不能進(jìn)行修改。因此,在多線程操作時(shí),可以認(rèn)為其是不變的,不用擔(dān)心其他線程有意或無意間對其進(jìn)行了修改。
String 類得不可變性使其性質(zhì)類似于只讀得共享文件,不用擔(dān)心因并發(fā)操作而導(dǎo)致的一系列問題,也就沒必要使用線程同步操作,簡單方便。

二、字符串的"+" 拼接

2.1 官方解釋

oracle jdk 8 的官方文檔中關(guān)于Sting 類的描述中有這么一段文字:

The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(or StringBuffer) class and its append method.

具體的翻譯是:

Java 語言提供對字符串串聯(lián)符號( " + " )以及將其他對象轉(zhuǎn)換為字符串的特殊支持。字符串串聯(lián)是通過 StringBuilder(或StringBuffer )類及其 append 方法實(shí)現(xiàn)的。

2.2 append() 方法

通過查看StringBuffer 類的源碼,可以得知:對于append(...) 方法,在StringBuffer 類中并沒有進(jìn)行覆寫,而是直接調(diào)用其父類的方法來實(shí)現(xiàn)。


在這里插入圖片描述

而StringBuffer 類的父類是AbstractStringBuilder 抽象類。

public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence

2.3 具體實(shí)現(xiàn)

在AbstractStringBuilder 抽象類中對于append(...) 方法的定義有很多,我們主要看參數(shù)為String 類型的方法,其他的均是類似的過程:

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
}

private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

String 中對于getChars(...) 方法的定義:

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

對于以上兩段源碼,其中,

  • ensureCapacityInternal(...) 方法的主要作用是擴(kuò)充value 數(shù)組的大小,其實(shí)現(xiàn)最后還是使用System.arraycopy(...) 方法實(shí)現(xiàn)對原數(shù)組的復(fù)制。
  • System.arraycopy(...) 方法的作用就是實(shí)現(xiàn)數(shù)組之間的復(fù)制。

2.4 源碼解析

2.4.1 方法解析

對于System.arraycopy(...) 方法:

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

參數(shù)解釋:

  • Object src : 源數(shù)組
  • int srcPos : 源數(shù)組的起始位置
  • Object dest : 目標(biāo)數(shù)組
  • int destPos : 目標(biāo)數(shù)組的起始位置
  • int length : 復(fù)制長度

實(shí)現(xiàn)功能:

將參數(shù)str 從下標(biāo)為srcPos 的位置開始,總共length 個(gè)字符( 在實(shí)際中不一定是str 的總長度 ) 復(fù)制到變量dest 中從下標(biāo)為destPos 開始的位置。

2.4.2 實(shí)際調(diào)用

在append(String str) 方法調(diào)用過程中,對于最后的System.arraycopy(...) 方法,其參數(shù)具體如下:

 System.arraycopy(String.value, 0, AbstractStringBuilder.value,  AbstractStringBuilder.count, str.length());

其中,

  • String.value實(shí)際上就是append(String str) 方法的參數(shù)str ;
  • 0 表示從str 下標(biāo)為0 的位置開始復(fù)制;
  • AbstractStringBuilder.value 實(shí)際上就是StringBuffer 的字符數(shù)組,也就是說str 字符串最后添加到了StringBuffer 的字符數(shù)組后面;
  • AbstractStringBuilder.count 是AbstractStringBuilder 類中字符數(shù)組value 的長度( 即StringBuffer 的字符數(shù)組的長度 ),表示第一個(gè)復(fù)制字符的存儲位置是value[count] ;
  • str.length() 顧名思義,指參數(shù)str 的長度,也就是說,此次復(fù)制是復(fù)制整個(gè)str 字符串。

也就是說,在實(shí)際的運(yùn)行過程中,調(diào)用的System.arraycopy(...) 方法是這樣子的:

System.arraycopy(str, 0, value, count, len);

因?yàn)閏ount= value.length 并且 len=str.length,所以該方法實(shí)現(xiàn)的功能是:
將字符串str 拼接到字符數(shù)組value 之后,這里的value 指的是AbstractStringBuilder 類中的字符數(shù)組value ,也是StringBuffer 的字符數(shù)組。

2.5 轉(zhuǎn)換成String

經(jīng)過前面的幾步,字符串str 已經(jīng)成功拼接到了StringBuffer 的字符數(shù)組之后了,但是StringBuffer 又是怎么轉(zhuǎn)換成String 類的呢?
其實(shí)很簡單,只需要調(diào)用StringBuffer 的toString() 方法即可。
StringBuffer 類中對于toString() 方法的定義如下:

 @Override
public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

String 類的構(gòu)造方法:

String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

由上述源碼可以發(fā)現(xiàn),當(dāng)調(diào)用StringBuffer 類的toString() 方法時(shí),會自動新建一個(gè)String 對象,用以存儲StringBuffer 類中字符數(shù)組的值。

三、總結(jié)

3.1 String 類的不可變性

String 類是不可變類的原因:

  • String 類的值封裝在字符數(shù)組value 中,而value 是被final 修飾的,不可以改變對象
  • 字符數(shù)組value 是私有變量,且沒有提供setter 方法
  • 除了構(gòu)造方法,在String 類中的方法里都沒有觸碰字符數(shù)組value 里的元素

3.2 使用"+" 進(jìn)行字符串拼接的過程

對于代碼

String str = new String("a") ;
String string = str + "b" ;

字符串拼接的全過程如下:

  1. 臨時(shí)創(chuàng)建一個(gè)StringBuffer 對象,并調(diào)用其append(String str) 方法進(jìn)行字符串的連接操作
  2. 調(diào)用StringBuffer 對象的toString() 方法轉(zhuǎn)換成String 對象,其內(nèi)容為“ab"
  3. 將生成的String 對象賦值給變量string

這里要注意一點(diǎn):

當(dāng)使用運(yùn)算符"+" 連接字符串時(shí),如果兩個(gè)操作數(shù)都是編譯時(shí)常量,則在編譯期就會計(jì)算出該字符串的值,而不會在運(yùn)行時(shí)創(chuàng)建StringBuffer 或 StringBuilder 對象。

在實(shí)際編碼時(shí),我們常提倡不使用"+" 反復(fù)進(jìn)行String 對象的連接,而是直接使用StringBuffer 或 StringBuilder 來連接的原因就是可以省去每次系統(tǒng)創(chuàng)建StringBuffer 或 StringBuilder的開銷。

至此,本文結(jié)束。我是陳冰安,一個(gè)Java學(xué)習(xí)者。歡迎關(guān)注我的公眾號【暗星涌動】,愿與你一同進(jìn)步。

?著作權(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ù)。

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