老生常談 String、StringBuilder、StringBuffer

poppy3721發表於2019-02-28

[TOC]

字串就是一連串的字元序列,Java提供了String、StringBuilder、StringBuffer三個類來封裝字串

String

String類是不可變類,String物件被建立以後,物件中的字元序列是不可改變的,直到這個物件被銷燬

為什麼是不可變的

jdk1.8
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //jdk1.9中將char陣列替換為byte陣列,緊湊字串帶來的優勢:更小的記憶體佔用,更快的操作速度。
    //建構函式
     public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    //建構函式
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    //返回一個新的char[]
    public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
 }
複製程式碼

根據上面的程式碼,我們看看String究竟是怎麼保證不可變的。

  • String類被final修飾,不可被繼承
  • string內部所有成員都設定為私有變數,外部無法訪問
  • 沒有向外暴露修改value的介面
  • value被final修飾,所以變數的引用不可變。
  • char[]·為引用型別仍可以通過引用修改例項物件,為此String(char value[])建構函式內部使用的copyOf而不是直接將value[]複製給內部變數`。
  • 在獲取value時,並沒有將value的引用直接返回,而是採用了arraycopy()的方式返回一個新的char[]
  • String類中的函式也處處透露著不可變的味道,比如:replace()
public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                //重新建立新的char[],不改變原有物件中的值
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                //最後返回新建立的String物件
                return new String(buf, true);
            }
        }
        return this;
    }
複製程式碼

當然不可變也不是絕對的,還是可以通過反射獲取到變value引用,然後通過value[]修改陣列的方式改變value物件例項

        String a = "Hello World!";
        String b = new String("Hello World!");
        String c = "Hello World!";

       //通過反射修改字串引用的value陣列
        Field field = a.getClass().getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(a);
        System.out.println(value);//Hello World!
        value[5] = `&`;
        System.out.println(value);//Hello&World!

        // 驗證b、c是否被改變
        System.out.println(b);//Hello&World! 
        System.out.println(c);//Hello&World!
複製程式碼

寫到這裡該如何引出不可變的好處呢?忘記反射吧,我們聊聊不可變的好處吧

不可變的優點

保證了執行緒安全

同一個字串例項可以被多個執行緒共享。

保證了基本的資訊保安

比如,網路通訊的IP地址,類載入器會根據一個類的完全限定名來讀取此類諸如此類,不可變性提供了安全性。

字串快取(常量池)的需要

具統計,常見應用使用的字串中有大約一半是重複的,為了避免建立重複字串,降低記憶體消耗和物件建立時的開銷。JVM提供了字串快取的功能——字串常量池。如果字串是可變的,我們就可以通過引用改變常量池總的同一個記憶體空間的值,其他指向此空間的引用也會發生改變。

支援hash對映和快取。

因為字串是不可變的,所以在它建立的時候hashcode就被快取了,不需要重新計算。這就使得字串很適合作為Map中的鍵,字串的處理速度要快過其它的鍵物件。這就是HashMap中的鍵往往都使用字串。

不可變的缺點

由於它的不可變性,像字串拼接、裁剪等普遍性的操作,往往對應用效能有明顯影響。

為了解決這個問題,java為我們提供了兩種解決方案

  • 字串常量池
  • StringBuilder、StringBuffer是可變的

字串常量池

還是剛才反射的示例

        String a = "Hello World!";
        String b = new String("Hello World!");
        String c = "Hello World!";
        //判斷字串變數是否指向同一塊記憶體
        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(b == c);

        // 通過反射觀察a, b, c 三者中變數value陣列的真實位置
        Field a_field = a.getClass().getDeclaredField("value");
        a_field.setAccessible(true);
        System.out.println(a_field.get(a));

        Field b_field = b.getClass().getDeclaredField("value");
        b_field.setAccessible(true);
        System.out.println(b_field.get(b));

        Field c_field = c.getClass().getDeclaredField("value");
        c_field.setAccessible(true);
        System.out.println(c_field.get(c));
        //通過反射發現String物件中變數value指向了同一塊記憶體
複製程式碼

輸出

false
true
false
[C@6f94fa3e
[C@6f94fa3e
[C@6f94fa3e
複製程式碼

字串常量的建立過程:

  1. 判斷常量池中是否存在”Hello World!”常量,如果有直接返回該常量在池中的引用地址
  2. 如果沒有,先建立一個char["Hello World!".length()]陣列物件,然後在常量池中建立一個字串物件並用陣列物件初始化字串物件的成員變數value,然後將這個字串的引用返回,比如賦值給a

由此可見,a和c物件指向常量池中相同的記憶體空間不言自明。

而b物件的建立是建立在以上的建立過程的基礎之上的。
"Hello World!"常量建立完成時返回的引用,會經過String的建構函式。

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
複製程式碼

建構函式內部將引用的物件成員變數value賦值給了內部成員變數value,然後將新建立的字元創物件引用賦值給了b,這個過程發生在堆中。

再來感受下下面這兩行程式碼有什麼區別

  String b = new String(a);
  String b = new String("Hello World!");
複製程式碼

StringBuilder和StringBuffer

二者都是可變的

為了彌補String的缺陷,Java先後提供了StringBuffer和StringBuilder可變字串類。

二者都繼承至AbstractStringBuilder,AbstractStringBuilder使用了char[] value字元陣列

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
}
複製程式碼

可以看出AbstractStringBuilder類和其成員變數value都沒有使用final關鍵字。

value陣列的預設長度

StringBuilder和StringBuffer的value陣列預設初始長度是16

    public StringBuilder() {
        super(16);
    }
    public StringBuffer() {
        super(16);
    }
複製程式碼

如果我們拼接的字串長度大概是可以預計的,那麼最好指定合適的capacity,避免多次擴容的開銷。

擴容產生多重開銷:拋棄原有陣列,建立新的陣列,進行arrycopy。

二者的區別

StringBuilder是非執行緒安全的,StringBuffer是執行緒安全的。

StringBuffer類中的方法使用了synchronized同步鎖來保證執行緒安全。
關於鎖的話題非常大,會單獨成文來說明,這裡推薦一篇不錯的部落格,有興趣的可以看看

JVM原始碼分析之synchronized實現

相關文章