前提介紹
在Java中String類的使用的頻率可謂相當高,它是Java語言中的核心類,在java.lang包下,主要用于字串的比較、查找、拼接等等操作,如果要深入理解一個類,最好的方法就是看看原始碼:
什么是字串
字串是由引號所括起來的一系列字符序列,
字串類(String)
/** String 類原始碼 */
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
……
}
從原始碼中,可以看出以下幾點:
- String類被final關鍵字修飾,表示String類不能被繼承,且它的屬性和方法都被 final 所修飾任何操作都會生成新物件,
String:: subString(),String::concat() 等方法都會生成一個新的String物件,不會在原物件上進行操作從下面String原始碼部分中很容易得到上面的結論 :
- String類實作了Serializable、CharSequence、 Comparable介面,
String類的值是通過char陣列存盤的,并且char陣列被private和final修飾,字串一旦創建就不能再修改,
String不可變性
- String物件一旦被創建就是固定不變的了,對String物件的任何改變都不影響到原物件,相關的任何操作都會生成新的物件,
- String不可變的表現就是當我們試圖對一個已有的物件 “abcd” 賦值為 “abcde”,String 會新創建一個物件,

注意點
這個無法被修改僅僅是指參考地址不可被修改( 也就是說堆疊里面的這個叫 value 的參考地址不可變 ,編譯器不允許我們把 value 指向堆中的另一個地址),并不代表存盤在堆中的這個陣列本身的內容不可變,

那既然我們說String是不可變的,那顯然僅僅靠final是遠遠不夠的:
- char陣列是private的,并且String類沒有對外提供修改這個陣列的方法,所以它初始化之后外界沒有有效的手段去改變它;
- String類被final修飾的,首先要講final修飾類的作用,被final修飾的類不能被繼承,類中的所有成員方法都會被隱式地指定為final方法,也就是不能擁有子類,成員方法也不能被重寫,
- String的所有方法里面,都很小心地避免去修改了char陣列中的資料,涉及到對char陣列中資料進行修改的操作全部都會重新創建一個String物件,
比如 substring 方法:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
為什么要設計成不可變的呢?
String被設計成不可變就是為了字串常量池,
- 字串常量池的定義
大量頻繁的創建字串,將會極大程度地影響程式的性能,字串的分配和其他物件分配一樣,是需要消耗高昂的時間和空間的,而且字串我們使用的非常多,JVM為了提高性能和減少記憶體的開銷,所以在實體化字串的時候使用字串常量池進行優化,
- JVM為了提高性能和減少記憶體開銷,在實體化字串常量的時候進行了一些優化:
字串開辟了一個字串常量池 String Pool(HashSet的StringTable),可以理解為快取區創建字串常量時,首先檢查字串常量池中是否存在該字串,
池化思想其實在Java中并不少見,字串常量池也是類似的思想,當創建字串時,JVM會首先檢查字串常量池,如果該字串已經存在常量池中,那么就直接回傳常量池中的實體參考,如果字串不存在常量池中,就會實體化該字串并且將其放到常量池中 ,
堆記憶體中只會創建一個 String 物件:
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2) // true

String允許被改變,那如果我們修改了 str2 的內容為 good,那么 str1 也會被修改,顯然這不是我們想要看見的結果,
new String(“abc”)創建了幾個物件?
- 如果之前"abc"字串沒有使用過,毫無疑問是創建兩個物件,堆中創建了一個String物件,字串常量池創建了一個,一共兩個,
- 如果之前已經使用過了"abc"字串,則不會再在字串常量池創建物件,而是從字串常量緩沖區中獲取,只會在堆中創建一個String物件
String s1 = "abc";
String s2 = new String("abc");
//s2這行代碼,只會創建一個物件
String被設計成不可變就是為了安全
- 作為最基礎最常用的資料型別,String 被許多Java類別庫用來作為引數,如果 String不是固定不變的,安全性考慮,字串應用場景眾多,設計成不可變性可以有效防止字串被有意篡改
- String被許多的Java類(庫)用來當做引數,比如網路連接地址URL,檔案路徑path,還有反射機制所需要的String引數等,假若String不是固定不變的,將會引起各種安全隱患,
- 在多執行緒環境下,眾所周知,多個執行緒同時想要修改同一個資源,是存在危險的,而String作為不可變物件,不能被修改,并且多個執行緒同時讀同一個資源,是完全沒有問題的,所以String是執行緒安全的,
String被設計成不可變就是為了效率
字串不變性保證了hash碼的唯一性,因此可以放心的進行快取,這也是一種性能優化手段,意味著不必每次都取計算新的哈希碼,
String真的不可變嗎?
- String無非就是改變 char 陣列 value 的內容,而 value 是私有屬性,那么在 Java中有沒有某種手段可以訪問類的私有屬性呢?
- 反射,使用反射可以直接修改 char 陣列中的內容,當然,一般來說我們不這么做,
看下面代碼

字串的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 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);
}
}
return this;
}
其他方法也是一樣,無論是sub、concat還是replace操作都不是在原有的字串上進行的,而是重新生成了一個新的字串物件,
字串拼接
字串的拼接在Java中是很常見的操作,但是拼接字串并不是簡簡單單地使用"+"號即可,還有一些要注意的點,否則會造成效率低下,
public static void main(String[] args) throws Exception {
String s = "";
for (int i = 0; i < 10; i++) {
s+=i;
}
System.out.println(s);//0123456789
}
在回圈內使用+=拼接字串會有什么問題呢?我們反編譯一下看看就知道了,

- 其實反編譯后,我們可以看到String類使用"+="拼接的底層其實是使用StringBuilder,先初始化一個StringBuilder物件,然后使用append()方法拼接,最后使用toString()方法得到結果,
- 問題在于如果在回圈體內使用+=拼接,會創建很多臨時的StringBuilder物件,拼接后再呼叫toString()賦給原String物件,這會生成大量臨時物件,嚴重影響性能,
所以在回圈體內進行字串拼接時,建議使用StringBuilder或者StringBuffer類,例子如下:
public static void main(String[] args) throws Exception {
StringBuilder s = new StringBuilder();
for (int i = 0; i < 10; i++) {
s.append(i);
}
System.out.println(s.toString());//0123456789
}
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);
}
StringBuilder和StringBuffer的區別在于,StringBuffer的方法都被sync關鍵字修飾,所以是執行緒安全的,而StringBuilder則是執行緒不安全的(效率高),
總結
并不是因為char陣列是final才導致String的不可變,而是為了把String設計成不可變才把 char 陣列設定為 final 的,
所有不可變類都完全遵守這些規則:
不要提供setter方法(包括修改欄位的方法和修改欄位參考物件的方法);
將類的所有欄位定義為 final、private 的;
不允許子類重寫方法,簡單的辦法是將類宣告為 final,更好的方法是將建構式宣告為私有的,通過工廠方法創建物件;
如果類的欄位是對可變物件的參考,不允許修改被參考物件,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/286905.html
標籤:其他
