App下載

Java面試題:談?wù)凷tring、StringBuffer、StringBuilder的區(qū)別?

猿友 2020-09-14 14:47:23 瀏覽數(shù) (3560)
反饋

文章來(lái)源于公眾號(hào):程序新視界 作者:丑胖俠二師兄

關(guān)于字符串的面試題除了內(nèi)存分布、equals 比較,最常見(jiàn)的就是與StringBufferStringBuilder之間的區(qū)別了。

如果你回答:String 類(lèi)是不可變的,StringBufferStringBuilder是可變類(lèi),StringBuffer是線程安全的,StringBuilder則不是線程安全的。

就上面的總結(jié)而言,好像知道的有點(diǎn)少。本篇文章就帶領(lǐng)大家全面的了解一下它們?nèi)齻€(gè)的區(qū)別與底層實(shí)現(xiàn)。

String字符串的拼接

關(guān)于String字符串前面多篇文章已經(jīng)詳細(xì)描述過(guò),它的不可變性也是因?yàn)槊慨?dāng)通過(guò)“+”操作時(shí),都會(huì)在內(nèi)存中生成新的字符串而導(dǎo)致的。

String a = "hello ";
String b = "world!";
String ab = a + b;

針對(duì)上述代碼,內(nèi)存分布圖如下:

String、StringBuffer、StringBuilder的區(qū)別

其中 a 和 b 初始化時(shí)位于字符串常量池,ab 拼接后的對(duì)象位于堆中??梢院苤庇^的看出,經(jīng)過(guò)拼接新生成了String對(duì)象。如果拼接多次,那么會(huì)生成多個(gè)中間對(duì)象。

上面的結(jié)論在Java8之前是成立的,在Java8時(shí) JDK 對(duì)“+”號(hào)拼接進(jìn)行了優(yōu)化,上面所寫(xiě)的拼接方式會(huì)被優(yōu)化為基于StringBuilderappend方法進(jìn)行處理。

stack=2, locals=4, args_size=1
     0: ldc           #2                  // String hello
     2: astore_1
     3: ldc           #3                  // String world!
     5: astore_2
     6: new           #4                  // class java/lang/StringBuilder
     9: dup
    10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
    13: aload_1
    14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    17: aload_2
    18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    24: astore_3
    25: return

上面是通過(guò) javap -verbose命令反編譯字節(jié)碼的結(jié)果,很顯然可以看到StringBuilder的創(chuàng)建和`append方法的調(diào)用。

此時(shí),如果再籠統(tǒng)的回答:通過(guò)加號(hào)拼接字符串會(huì)創(chuàng)建多個(gè)String對(duì)象,因此性能比StringBuilder差,就是錯(cuò)誤的了。因?yàn)楸举|(zhì)上加號(hào)拼接的效果最終經(jīng)過(guò)編譯器處理之后和StringBuilder是一致的。

如果你在代碼中使用如下寫(xiě)法:

StringBuilder sb = new StringBuilder("hello ");
sb.append("world!");
System.out.println(sb.toString());

編譯器的插件甚至建議你使用String來(lái)代替。

StringBuffer與StringBuilder的對(duì)比

StringBufferStringBuilder實(shí)現(xiàn)的核心代碼基本一致,很多代碼都是公用的。這兩個(gè)類(lèi)均繼承自抽象類(lèi)AbstractStringBuilder。

我們來(lái)從構(gòu)造方法到append方法來(lái)逐一看一下它們的區(qū)別。先看StringBuilder的構(gòu)造方法:

public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}

其中super方法便是調(diào)用的AbstractStringBuilder的構(gòu)造方法。對(duì)應(yīng)StringBuffer的構(gòu)造方法中實(shí)現(xiàn)也是如此:

public StringBuffer(String str) {
    super(str.length() + 16);
    append(str);
}

從構(gòu)造方法來(lái)說(shuō),StringBufferStringBuilder是一樣的。下面再看看append方法,StringBuilder實(shí)現(xiàn)如下:

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

StringBuffer對(duì)應(yīng)的方法如下:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

很顯然,在StringBufferappend方法實(shí)現(xiàn)上除了內(nèi)部將toStringCache變量賦值為null,唯一的不同就是在方法上使用synchronized進(jìn)行了同步處理。

toStringCache是用來(lái)緩存最后一次調(diào)用toString方法時(shí)生成的字符串,當(dāng)StringBuffer內(nèi)容變動(dòng)時(shí),該值也會(huì)變動(dòng)。

通過(guò)上面的append方法的對(duì)比,我們可以很輕易的發(fā)現(xiàn)StringBuffer是線程安全的,StringBuilder是非線程安全的。當(dāng)然,使用synchronized進(jìn)行同步處理,性能便會(huì)降低很多。

StringBuffer與StringBuilder的底層實(shí)現(xiàn)

StringBufferStringBuilder都調(diào)用了父類(lèi)的構(gòu)造方法:

AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}

通過(guò)該構(gòu)造方法我們可以看到它們用來(lái)處理字符串信息的關(guān)鍵屬性為value。在初始化時(shí)先初始化一個(gè)長(zhǎng)度為傳入字符串長(zhǎng)度+16的char[]數(shù)組,也就是value值,用來(lái)存儲(chǔ)實(shí)際的字符串。

在調(diào)用父類(lèi)構(gòu)造方法之后便是調(diào)用各自的append方法(見(jiàn)前面的代碼),而其中的核心處理又的調(diào)用父類(lèi)的append方法:

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;
}

上述代碼中其中str.getChars方法用來(lái)對(duì)傳入的str字符串進(jìn)行拼接,在原有的value數(shù)組后面進(jìn)行填充。而count用來(lái)記錄當(dāng)前value數(shù)字中已經(jīng)使用的長(zhǎng)度。

String、StringBuffer、StringBuilder的區(qū)別

那么,當(dāng)沒(méi)有使用synchronized進(jìn)行同步操作時(shí),線程不安全發(fā)生在哪里?上面代碼中count+=len并不是原子操作。比如當(dāng)前count為 5,兩個(gè)線程同時(shí)執(zhí)行到 ++ 操作,拿到的值都為 5,執(zhí)行完加操作之后賦值給count,兩個(gè)線程賦值都為 6,而不是 7。此時(shí)便出現(xiàn)了線程不安全的問(wèn)題。

為什么String要設(shè)計(jì)成不可變

Java 中將String設(shè)計(jì)成不可變的是綜合考慮到各種因素的結(jié)果,有如下原因:

1、字符串常量池的需要,如果字符串可變,改變一個(gè)對(duì)象會(huì)影響到另外一個(gè)獨(dú)立的對(duì)象。不變這也是字符串常量池存在的前提條件。

2、JavaString對(duì)象的哈希碼被頻繁地使用,比如在HashMap等容器中。字符串不變保證了hash碼的唯一性,可以方向緩存并使用。

3、安全性,確保String在當(dāng)做參數(shù)傳遞時(shí)保持不變,避免安全隱患。比如在數(shù)據(jù)庫(kù)用戶名、密碼、訪問(wèn)路徑等傳輸過(guò)程中的保持不變,防止改變字符串指向?qū)ο蟮闹当桓淖儭?/p>

4、由于字符串變量不可變,在多線程中可以被共享使用。

小結(jié)

單純的死記硬背面試題我們都會(huì),但要在記憶面試題的過(guò)程中了解更多底層實(shí)現(xiàn)原理,不僅僅有助于理解“為什么”,同時(shí)還能學(xué)到更多相關(guān)的知識(shí)和原理。

在本文中簡(jiǎn)化了StringBuilderStringBuffer內(nèi)部數(shù)據(jù)的 copy 、數(shù)組擴(kuò)容等步驟的講解,感興趣的朋友可以繼續(xù)對(duì)照源碼進(jìn)行深入研究。

以上就是W3Cschool編程獅關(guān)于Java面試題:談?wù)凷tring、StringBuffer、StringBuilder的區(qū)別?的相關(guān)介紹了,希望對(duì)大家有所幫助。

0 人點(diǎn)贊