文中相關原始碼: String.java
今天來說說 String
。
貫穿全文,你需要始終記住這句話,String 是不可變類
。其實前面說過的所有基本資料型別包裝類都是不可變類,但是在 String
的原始碼中,不可變類
的概念體現的更加淋漓盡致。所以,在閱讀 String
原始碼的同時,抽絲剝繭,你會對不可變類有更深的理解。
什麼是不可變類 ?
首先來看一下什麼是不可變類?Effective Java 第三版
第 17 條 使不可變性最小化
中對 不可變類
的解釋:
不可變類是指其例項不能被修改的類。每個例項中包含的所有資訊都必須在建立該例項的時候就提供,並在物件的整個生命週期 (lifetime) 內固定不變 。
為了使類成為不可變,要遵循下面五條規則:
- 不要提供任何會修改物件狀態的方法(也稱為設值方法) 。
- 保證類不會被擴充套件。 為了防止子類化,一般做法是宣告這個類成為 final 的。
- 宣告所有的域都是 final 的。
- 宣告所有的域都為私有的。 這樣可以防止客戶端獲得訪問被域引用的可變物件的許可權,並防止客戶端直接修改這些物件 。
- 確保對於任何可變元件的互斥訪問。 如果類具有指向可變物件的域,則必須確保該類的客戶端無法獲得指向這些物件的引用 。 並且,永遠不要用客戶端提供的物件引用來初始化這樣的域,也不要從任何訪問方法( accessor)中返回該物件引用 。 在構造器、訪問方法和 readObject 方法(詳見第 88 條)中請使用保護性拷貝( defensive copy )技術(詳見第50 條) 。
根據這五條原則,來品嚐一下 String.java
吧!
類定義
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
就成了效能瓶頸,這也催生了 StringBuffer
和 StringBuilder
。後面會單獨分析。
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 題解,歡迎關注!