Java中的這些String特性可能需要了解下

落叶微风發表於2024-05-17

先總結下,String類具有以下特性:

  1. 不可變性(Immutable):String物件一旦建立就不能被修改。任何對String物件的操作都會返回一個新的String物件,原始物件保持不變。
  2. 字串表(String Table):StringTable表是一種儲存字串常量的記憶體區域,它可以提高字串的重用率和效能。在建立字串時,如果字串已經存在於池中,則返回池中的字串物件,否則會建立一個新的字串物件並放入池中。
  3. 值傳遞:在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的探索與實踐,持續迭代中。

歡迎關注和點贊~

相關文章