走進 JDK 之 String

文中相關源碼: String.javajava

今天來講說 Stringgit

貫穿全文,你須要始終記住這句話,String 是不可變類 。其實前面說過的全部基本數據類型包裝類都是不可變類,可是在 String 的源碼中,不可變類 的概念體現的更加淋漓盡致。因此,在閱讀 String 源碼的同時,抽絲剝繭,你會對不可變類有更深的理解。github

什麼是不可變類 ?

首先來看一下什麼是不可變類?Effective Java 第三版 第 17 條 使不可變性最小化 中對 不可變類 的解釋:面試

不可變類是指其實例不能被修改的類。每一個實例中包含的全部信息都必須在建立該實例的時候就提供,並在對象的整個生命週期 (lifetime) 內固定不變 。數組

爲了使類成爲不可變,要遵循下面五條規則:安全

  1. 不要提供任何會修改對象狀態的方法(也稱爲設值方法) 。
  2. 保證類不會被擴展。 爲了防止子類化,通常作法是聲明這個類成爲 final 的。
  3. 聲明全部的域都是 final 的。
  4. 聲明全部的域都爲私有的。 這樣能夠防止客戶端得到訪問被域引用的可變對象的權限,並防止客戶端直接修改這些對象 。
  5. 確保對於任何可變組件的互斥訪問。 若是類具備指向可變對象的域,則必須確保該類的客戶端沒法得到指向這些對象的引用 。 而且,永遠不要用客戶端提供的對象引用來初始化這樣的域,也不要從任何訪問方法( accessor)中返回該對象引用 。 在構造器、訪問方法和 readObject 方法(詳見第 88 條)中請使用保護性拷貝( defensive copy )技術(詳見第50 條) 。

根據這五條原則,來品嚐一下 String.java 吧!bash

類定義

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {}
複製代碼

對應原則第二點 保證類不會被擴展,使用 final 修飾。此外:微信

  • 實現了 Serializable 接口,具有序列化能力
  • 實現了 Comparable 接口,具有比較對象大小能力,根據單字符的大小比較。
  • 實現了 CharSequence 接口,表示是一個字符序列,實現了該接口下的一些方法。

字段

private final char value[]; // 儲存字符串
private int hash; // 哈希值,默認爲 0
private static final long serialVersionUID = -6849794470754667710L; // 序列化標識
複製代碼

看起來 String 是一個獨立的對象,其實它是使用基本數據類型的數組 char[] 實現的。做爲使用者,咱們不須要打開 String 的黑匣子,直接根據它的 API 使用就能夠了,這正是 Java 的封裝性的體現。可是做爲開發者,咱們就有必要一探究竟了。函數

private final char value[] , 對應原則中第三條和第四條,聲明全部的域都是 final 的聲明全部的域都爲私有的。看到這裏,你大概明白了一點爲何 String 不可變。由於真正用來存儲字符串的字符數組是 final 修飾的,是不可變的。性能

構造函數

String 的構造函數不少,大體能夠分爲如下四種:

無參構造

public String() {
    this.value = "".value;
}
複製代碼

無參構造默認構建一個空字符串。鑑於 String 是不可變類,因此此構造器並無什麼意義,通常你也不會去構建一個不可變的空字符串對象。

參數是 byte[]

public String(byte bytes[]) {}
public String(byte bytes[], int offset, int length) {}
public String(byte bytes[], Charset charset) {}
public String(byte bytes[], String charsetName) {}
public String(byte bytes[], int offset, int length, Charset charset) {}
public String(byte bytes[], int offset, int length, String charsetName) {}
複製代碼

已經廢棄的就再也不列舉了。上面這些構造函數都差很少,最後都是調用 StringCoding.decode() 方法將字節數組轉換爲字符數組,再賦值給 value[]。這裏要注意一點,參數未指定編碼格式的話,默認使用系統的編碼格式,若是沒有獲取到系統編碼格式,則使用 ISO-8859-1 格式。

參數是 char[]

參數是 char[] 的構造函數有 3 個,逐個看一下:

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
複製代碼

爲了保證不可變性,並無直接賦值,this.value = value。而是使用 Arrays.copy() 方法將參數中的字符數組內容拷貝到 value[] 中。防止參數中字符數組的改變破壞了不可變性。

第二個:

public String(char value[], int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}
複製代碼

和上面的構造函數同樣,只是截取了參數中字符數組的一部分來構建字符串。

第三個:

/* * Package private constructor which shares value array for speed. * this constructor is always expected to be called with share==true. * a separate constructor is needed because we already have a public * String(char[]) constructor that makes a copy of the given char[]. * * 僅當前包可以使用。 * 直接將 this.value 指向參數中的 char[],再也不進行 copy 操做 * 性能好,節省內存,外包不可以使用,也不會破壞不可變性 */
    String(char[] value, boolean share) {
        // assert share : "unshared not supported";
        this.value = value;
    }
複製代碼

這裏的 share 通常只能爲 true,雖然並無使用到。增長這個參數是爲了和第一個構造函數區分開來,表示 value[] 共享了參數中的字符數組,由於這裏是直接賦值的,並無使用 Arrays.copy()。那這不是破壞了 String 的不可變性嗎?其實並無,由於你根本無法調用這個構造函數,它的包私有的。可是在 JDK 內部你能夠發現它的身影,

沒有了 copy 操做,大幅提升了效率。可是爲了保證不可變性,外部是不能調用的。

其餘構造函數

// 基於代碼點
public String(int[] codePoints, int offset, int count) {} 

// 基於 StringBuffer,須要同步
public String(StringBuffer buffer) {
    synchronized(buffer) {
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}

// 基於 StringBuilder,不須要同步
public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
複製代碼

方法

回頭再看一下 String 的不可變性,value[]private final 修飾的,這樣就真的能夠保證不可變嗎?

final char[] value = {'a','b','c'};
value[1] = 'd';
複製代碼

這是否是就垂手可得的打破了不可變性?final value[] 只能保證其引用不能再指向其餘內存地址,可是其真正的值仍是能夠改變的。因此僅僅經過一個 final 是沒法保證其值不變的,若是類自己提供方法修改實例值,那就沒有辦法保證不變性了。對應原則中第一條,不要提供任何會修改對象狀態的方法String 百分之百作到了這一點,它沒有對外提供任何能夠修改 value 的方法。

String 中有許多對字符串進行操做的函數,例如 substring concat replace replaceAll 等等,這些函數是否會修改類中的 value 域呢?下面就來看一看源碼。

substring(int beginIndex)

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    // beginIndex 不爲 0, 返回一個 新的 String 對象
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); 
}
複製代碼

concat(String str)

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); // 返回新的 String 對象
}
複製代碼

replace(char oldChar, char newChar)

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 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++;
               }
               return new String(buf, true); // 返回新的 String 對象
           }
    }
    return this;
}
複製代碼

String 類的方法實現都相對簡單,可是無一例外,它們絕對不會去修改 value[] 的值,須要返回 String 對象的話,都會從新 new 一個。正像原則第五條中所說的,確保對於任何可變組件的互斥訪問。 若是類具備指向可變對象的域,則必須確保該類的客戶端沒法得到指向這些對象的引用。

String.intern()

public native String intern();
複製代碼

這個方法比較特殊,是個本地方法。若是該字符串在常量池中已經存在,直接返回其引用。若是不存在,存入常量池再返回其引用。在下一篇文章中會進行詳細介紹。

其餘方法的源碼就不列舉了,感興趣的能夠到我上傳的 jdk 源碼 看看,String.java,添加了部分註釋。

不可變類的好處

從頭至尾都在說不可變類,那麼它有哪些好處呢?

  • 不可變對象比較簡單。
  • 不可變對象本質上是線程安全的,它們不要求同步。不可變對象能夠被自由地共享。
  • 不只能夠共享不可變對象,甚至也能夠共享它們的內部信息。
  • 不可變對象爲其餘對象提供了大量的構件。不管是可變的仍是不可變的對象。
  • 不可變對象無償地提供了失敗的原子性。

不可變類真正惟一的缺點是,對於每一個不一樣的值都須要一個單獨的對象。因此當須要大量字符串對象的時候,String 就成了性能瓶頸,這也催生了 StringBufferStringBuilder。後面會單獨分析。

String 真的不可變嗎 ?

學習就是本身不斷打本身臉的過程。真的沒有辦法修改 String 對象的值嗎?答案確定是否認的,反射機制能夠作到不少日常作不到的事情。

String str = "123";
System.out.println(str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = '3';
System.out.println(str);
複製代碼

執行結果:

123
133
複製代碼

經過反射,的確修改了 value[] 的值。

總結

藉着 String 源碼,說了說 不可變類。簡單總結一下 String 作了哪些措施來保證不可變性:

  • value[] 使用 private final 修飾
  • 構造函數中複製實參的值給 value[]
  • 不對外提供任何修改 value[] 值的方法
  • 須要返回 String 的方法,毫不返回原對象,都是從新 new 一個 String 返回

下一篇仍是寫 String , 說說 String 在內存中的位置和字符串常量池的一些知識,以及 String 相關的常見面試題。

文章首發於微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

相關文章
相關標籤/搜索