先總結下,String類具有以下特性:
- 不可變性(Immutable):String物件一旦建立就不能被修改。任何對String物件的操作都會返回一個新的String物件,原始物件保持不變。
- 字串表(String Table):StringTable表是一種儲存字串常量的記憶體區域,它可以提高字串的重用率和效能。在建立字串時,如果字串已經存在於池中,則返回池中的字串物件,否則會建立一個新的字串物件並放入池中。
- 值傳遞:在Java中,String物件是透過值傳遞的方式傳遞的。這意味著當將一個字串傳遞給方法或賦值給另一個變數時,實際上傳遞的是字串的副本而不是原始字串物件。
下文將詳細說明這些特性。
本文基於JDK17說明。
不可變性(Immutable)
String的不可變性指的是一旦建立了String物件,它的值就不能被修改。
這意味著在任何對String物件進行操作時,都會返回一個新的String物件,而原始物件的值保持不變。
這種特性有助於保護資料的一致性,並且在多執行緒環境下也更加安全。
下面是一個示例來說明String的不可變性:
public class ImmutableStringExample {
public static void main(String[] args) {
String original = "Hello";
String modified = original.concat(", World!");
System.out.println("Original string: " + original);
System.out.println("Modified string: " + modified);
}
}
輸出結果為:
Original string: Hello
Modified string: Hello, World!
在這個例子中,雖然使用了 concat
方法對原始字串進行了修改,但是原始字串 original
的值並沒有改變。相反,concat
方法返回了一個新的字串物件,其中包含了修改後的值。
其中concat
函式的主要程式碼如下:
@ForceInline
static String simpleConcat(Object first, Object second) {
String s1 = stringOf(first);
String s2 = stringOf(second);
if (s1.isEmpty()) {
// 直接返回s2引數
return new String(s2);
}
if (s2.isEmpty()) {
// 直接返回s1引數
return new String(s1);
}
// start "mixing" in length and coder or arguments, order is not
// important
long indexCoder = mix(initialCoder(), s1);
indexCoder = mix(indexCoder, s2);
byte[] buf = newArray(indexCoder);
// prepend each argument in reverse order, since we prepending
// from the end of the byte array
indexCoder = prepend(indexCoder, buf, s2);
indexCoder = prepend(indexCoder, buf, s1);
// 返回新建的String物件
return newString(buf, indexCoder);
}
String的不可變性對於設計具有很多優點。
- 提供了一種簡單且安全的資料結構,因為任何時候都可以確信一個String物件的值不會在不經意間被改變。
- 由於String是不可變的,所以它們可以被安全地共享,而不必擔心在共享的過程中被修改。
- String的不可變性也有助於提高字串操作的效能,因為它可以避免頻繁的複製和重建字串物件。
String的不可變性使得它在Java中成為一種簡單、安全且高效的資料結構。
不可變性怎麼保證的
String 的不可變性是透過類的設計、內部實現和方法設計來保證的,這種不可變性使得 String 物件在多執行緒環境下更加安全,並且可以被方便地共享和重用。
String 的不可變性是透過以下幾種方式來保證的:
- String 類的設計:String 類被設計為 final 類,這意味著它不能被繼承,也就是說無法建立 String 的子類來修改其行為。這樣就防止了透過繼承來修改 String 類的方法來改變其不可變性。
- String 物件的內部實現:String 物件內部使用位元組陣列
byte[]
來儲存字串的值,而且這個位元組陣列是被宣告為final
的,即不可修改。一旦一個 String 物件被建立,它的值就會被儲存在這個位元組陣列中,而且這個值是無法被修改的。也沒對外提供get、set方法。 - String 類的方法:String 類中的方法都被設計成不會修改原始物件的值,而是返回一個新的 String 物件,其中包含了修改後的值。比如,對於字串連線操作
concat()
、子串提取substring()
、大小寫轉換toUpperCase()
和toLowerCase()
等方法,都會返回一個新的 String 物件,而不會修改原始字串。
如下是String物件的部分原始碼,可以看到value和物件都被final修飾。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
// ...
}
值傳遞
在Java中,String物件的傳遞是透過值傳遞(pass by value)進行的。
這意味著在將String物件傳遞給方法或賦值給另一個變數時,傳遞的是物件的副本而不是物件本身。
當你將一個String物件傳遞給方法時,實際上傳遞的是物件的引用的副本,而不是物件本身。這意味著方法內部的操作不會影響原始的String物件,因為它們操作的是副本。
下面是一個示例來說明String的值傳遞:
public class StringValuePassingExample {
public static void main(String[] args) {
String original = "Hello";
modifyString(original);
System.out.println("Original string after method call: " + original);
}
public static void modifyString(String str) {
str = str + ", World!";
System.out.println("Modified string inside method: " + str);
}
}
輸出結果為:
Modified string inside method: Hello, World!
Original string after method call: Hello
在這個例子中,雖然在 modifyString
方法內部對 str
進行了修改,但原始的 original
字串並沒有受到影響。這是因為在方法呼叫時,傳遞的是 original
字串的副本,而不是原始物件本身。
因此,在方法內部對 str
的任何修改都不會影響原始的 original
字串。
字串儲存在StringTable
StringTable是一種特殊的記憶體區域,用於儲存字串常量。
當建立字串時,如果該字串已經存在於StringTable中,則直接返回對該字串的引用,而不會建立新的字串物件;如果該字串不在StringTable中,則會建立一個新的字串物件,並將其新增到StringTable中。
如果字串是動態建立的,比如透過new、concat、substring、toUpperCase動態建立的會放到堆記憶體中。
StringTable、字串、堆的示意圖如下所示:
StringTable的設計有幾個主要原因:
- 節省記憶體空間:由於字串常常是應用程式中使用的不可變的常量,因此可以被多個字串引用。透過StringTable,可以確保相同的字串常量在記憶體中只有一份複製,從而節省記憶體空間。
- 提高效能:由於字串常量在記憶體中只有一份複製,所以可以透過比較字串的引用地址來比較字串的內容,而不必比較字串的實際內容,這樣可以提高比較字串的效率。
- 保證字串的唯一性:透過StringTable,可以確保在應用程式中使用的字串常量是唯一的,這有助於減少由於字串拼接等操作而引起的錯誤。
- 方便字串的共享和重用:由於StringTable中的字串常量是唯一的,因此可以方便地共享和重用字串常量,從而提高應用程式的效能和效率。
字串比拼
import java.util.HashMap;
public class StringTableDemo {
public static void main(String[] args) {
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1 == str2);//false
String str3 = new String("abc");
System.out.println(str3 == str2);//false
String str4 = "a" + "b";
System.out.println(str4 == "ab");//true
String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab");//false
String str7 = "abc".substring(0, 2);
System.out.println(str7 == "ab");//false
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");//false
String s5 = "a";
String s6 = "abc";
String s7 = s5 + "bc";
System.out.println(s6 == s7.intern());//true
}
}
透過以上的例子可以總結出以下規律:
- 單獨使用
""
引號建立的字串都是常量,編譯期就已經確定儲存到StringPool中。 - 使用new String("")建立的物件會儲存到heap中,是執行期新建立的。
- 使用只包含常量的字串連線符如"aa"+"bb"建立的也是常量,編譯期就能確定已經儲存到StringPool中。
- 使用包含變數的字串連線如"aa"+s建立的物件是執行期才建立的,儲存到heap中。
- 執行期呼叫String的
intern()
方法可以向String Pool中動態新增物件。
關於作者
來自全棧程式設計師nine的探索與實踐,持續迭代中。
歡迎關注和點贊~