老生常談 String、StringBuilder、StringBuffer

[TOC]java

字符串就是一連串的字符序列,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中的鍵每每都使用字符串。ui

不可變的缺點

因爲它的不可變性,像字符串拼接、裁剪等廣泛性的操做,每每對應用性能有明顯影響。this

爲了解決這個問題,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實現

相關文章
相關標籤/搜索