String 、StringBuilder 與 StringBuffer 之間的區別

String


初始化字符串:

String str = "so easy!";

這是字符串初始化的 “簡介版本”,通常字符串的初始化有三種方式:

// 1.最常見,簡短
String str1 = "Hello wrold!";
// 2.構造函數初始化
String str2 = new String("Hello World!");
// 3.字符數組初始化
char[] charArray = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
String str3 = new String(charArray);

然後我們一起看一下 String 類的源碼,再做分析。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = new char[0];
    }

    /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    
    ...
}

private final char value[]; ,可以觀察字符串本身是保存在 char 數組中的,換句話說,String 對象其實就是一個字符數組。

這裏需要注意的一件非常重要的事情是,String 類被 final 關鍵字所修飾,public final class String{}, 這意味着 String 是不可變(immutable)類型。

不可變(immutable) 意味着什麼呢?

String str1 = "Hello World!";
str1.substring(1,4).concat("cbl").toLowerCase().trim().replace('b', 'a');
System.out.println(str1); // Hello World!

上面這段程序的輸出依舊是 Hello World!

因爲字符串是 final 的,所以對 str1 所進行的一系列操作並不會改變 str1 本身。這一系列操作返回的是更改後的新字符串,並不會改變 str1 本身。

我們可以看一下 concat() 的源碼:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

原始字符串永遠不會更改。而是複製一份,並將要連接的文本被添加到複製的字符串之後,最後返回一個新的字符串。

我們用一個變量接收一下返回值,然後輸出,發現返回了一個新的字符串 local

String str1 = "Hello World!";
String str2 = str1.substring(3,5).concat("cbl").toLowerCase().trim().replace('b', 'a');
System.out.println(str2); // local

JVM 的原生內存模型爲:

方便我們對 String 、StringBuffer 和 StringBuilder 的理解,我們僅關心下面圖示中的內存區域:

其中 Stack 表示棧區,Heap 表示堆區,String Pool 表示字符串池。

我們再來看看這兩個字符串:

String str1 = "algorithm"
String str2 = "algorithm"

實例化的字符串 str1str2 的值保存在 Java 堆內存中,堆內存用於爲所有 Java 對象動態分配內存。雖然  str1str2 是兩個不同的引用變量,但它們都指向 Java 堆內存中的同一塊內存位置。

雖然看起來有兩個不同的 String 對象,但實際上只有一個。str2 從未被實例化爲對象,而是引用了在堆內存中分配給 str1 的對象。

這是由於 Java 針對字符串進行優化的方式造成的。每次實例化字符串對象時,都會將添加到堆內存的值與之前已添加的值進行比較。如果堆內存中已存在相等的值,則不會初始化該對象,而是將該值賦給引用變量。

str1 這樣的常量值保存在一個稱爲常量字符串的池中,簡稱 String Pool,其中包含所有的常量字符串。不過,使用關鍵字 new 創建的字符串 “偷偷” 繞過了 String Pool,而是直接存儲在 Java Heap 當中。

再看一個簡單的例子:

String str1 = "algorithm";
String str2 = "algorithm";
String str3 = new String("algorithm");

System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // false

因爲 str1str2 指向內存中的同一對象, str1 == str2 返回爲 truestr1.equals(str2) 也返回 true

因爲 str3  使用關鍵字 new 顯式實例化,所以 String Pool 中雖然已存在字符串文本,依舊會使用構造函數創建一個新對象。

equals() 方法比較的兩個變量的值,而不是它們所指向的內存地址,這就是 str1.equals(str3)str1.equals(str2) 都返回 true 的原因。而 == 判斷的是兩個變量所指向的內存地址,比較的是真正意義上的指針操作,所以 str1 == str2 返回 true ,而 str1 == str3 返回 false

PS:毫不誇張地說,我的一個在阿里實習過的大佬同學,在面試某國企時就跪在了 equals 和 == 的區別上

需要注意 substring()concat() 方法返回一個新的 String 對象,並將其保存在字符串池中。

我們舉的例子都比較簡單,但如果我們考慮一些使用數百個字符串變量和數千個操作(如 substring() 或concat())的大型項目,僅使用 String 類可能會導致嚴重的內存泄漏和時間延遲。

這正是爲什麼還要設計 StringBuffer 或 StringBuilder 類的原因。

StringBuffer & StringBuilder

可變性(mutablity)

StringBufferStringBuilder 對象與 String 對象一樣,三者都是字符序列。但是 StringBufferStringBuilder 是可變的(mutable),String 是不可變的(immutable),這意味着一旦我們爲 StringBufferStringBuilder 初始化一個值,該值就會作爲 StringBufferStringBuilder 對象的屬性進行處理。

無論我們修改 StringBufferStringBuilder 的值多少次,都不會創建新的 String、StringBuffer 或 StringBuilder 對象。StringBufferStringBuilder 的時間效率更高,資源消耗更少。

StringBuilder vs StringBuffer

這兩個類幾乎完全相同,它們使用同名的方法,返回相同的結果。但它們之間有兩個主要區別:

方法

StringBuilderStringBuffer 擁有相同的方法(除 StringBuffer 的方法都是 synchronized 的)。兩者常用的幾個方法:append()insertreplace()delete()reverse()

String vs StringBuilder vs StringBuffer

注意:從上表中看到的,String 類在時間和內存上的效率都較低,但這並不意味着我們不應該再使用它。

事實上,String 可以非常方便地使用,因爲它可以快速編寫,我在實際開發經常使用 public static final String 來定義字符串常量。

String concatString = "concatString";
StringBuffer appendBuffer = new StringBuffer("appendBuffer");
StringBuilder appendBuilder = new StringBuilder("appendBuilder");
long timerStarted;

timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    concatString += " another string";
}
System.out.println("50000次String concat 操作的時間:" + (System.currentTimeMillis() - timerStarted) + "ms");

timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    appendBuffer.append(" another string");
}
System.out.println("50000次 StringBuffer 的 append 操作時間:" + (System.currentTimeMillis() - timerStarted) + "ms");
        
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    appendBuilder.append(" another string");
}
System.out.println("50000次 StringBuilder 的 append 操作時間:" + (System.currentTimeMillis() - timerStarted) + "ms");

輸出:

50000次String concat 操作的時間18108ms
50000次 StringBuffer  append 操作時間7ms
50000次 StringBuilder  append 操作時間3ms

根據您的 Java 虛擬機的不同,此輸出可能會有所不同。僅從這個測試結果可以看出,StringBuilder 在字符串操作方面是最快的。速度次快的是 StringBuffer ,它比 StringBuilder 慢 2~3 倍。Stringconcat 操作最慢。

連接同樣多的字符串,StringBuilderString 快約 6000 倍,換句話說,StringBuilder 在 1 秒內可以連接的字符串,String 需要 1.6 小時。

總結

**可變性:**String 是不可變類型,如果試圖更改 String 對象的值,則會在 String Pool 中創建另一個對象,而 StringBuffer 和 StringBuilder 是可變的,其值可以更改。

**線程安全:**StringBuffer 和 StringBuilder 之間的區別就在於 StringBuffer 是線程安全的。因此,當應用程序只需要在單線程中運行時,最好使用 StringBuilder ,因爲 StringBuilder 比 StringBuffer 更高效。

使用建議:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2jLN2X2V0y2KtRryhGSHGg