Java String常量池

geekartt發表於2019-01-19

1. String例項的初始化

String型別的初始化在Java中分為兩類:

  • 一類是通過雙引號包裹一個字元來初始化;
  • 另一類是通過關鍵字new 像一個普通的物件那樣初始化一個String例項。

前者在constant pool中開闢一個常量,並返回相應的引用,而後者是在heap中開闢一個常量,再返回相應的物件。所以,兩者的reference肯定是不同的:

public static void main(String... args) {
    String s1 = "abcd";
    String s2 = new String("abcd");
    System.out.println(s1 == s2);   // false
}

而constant pool中的常量是可以被共享用於節省記憶體開銷和建立時間的開銷(這也是引入constant pool的原因)。例如:

public static void main(String... args) {
    String s1 = "abcd";
    String s2 = "abcd";
    System.out.println(s1 == s2);   // true
}

結合這兩者,其實還可以回答另外一個常見的面試題目:

public static void main(String... args) {
    String s = new String("abcd");
}

這句話建立了幾個物件?

首先毫無疑問,"abcd"本身是一個物件,被放於常量池。而由於這裡使用了new關鍵字,所以s得到的物件必然是被建立在heap裡的。所以,這裡其實一共建立了2個物件。

但tricky的部分是,如果在這個函式被呼叫前的別的地方,已經有了"abcd"這個字串,那麼它就事先在constant pool中被建立了出來。此時,這裡就只會建立一個物件,即建立在heap的new String("abcd")物件。

但String的記憶體分配,遠遠沒有這麼簡單。對於String的拼接,需要做更深入的理解和思考。

2. String的拼接

下面看一個問題:

public static void main(String... args) {
    String s1 = "hell" + "o";
    String s2 = "h" + "ello";
    System.out.println(s1 == s2);   // true
    System.out.println(s1 == "hello");  // true
    System.out.println(s2 == "hello");  // true
    System.out.println("hello" == "hello"); // true
    
    // ------------------------
    
    String c1 = "ab";
    String c2 = c1 + "c";
    System.out.println(c2 == "abc");  // false
    
}

前面四個輸出其實很容易理解,最終的結果,都指向了constant pool裡的一個常量"hello"。但奇怪的是,最後一個輸出也是"abc",並且還都是用指向constant pool中常量的變數來做的拼接,但卻得到了一個false的結果。

原因是,Java的String拼接有兩個規則:

  • 對於拼接的值,如果都是雙引號包裹字串的形式,則將結果放於constant pool,如果constant pool已經有這個值了,則直接返回這個已有值。
  • 而如果拼接的值中,有一個是非雙引號包裹字串的形式,則從heap中開闢一個新的區域儲存常量。也即是使用了String變數來做拼接的情況。

在這樣的大原則下,對宣告為final的String變數需要做更多的考慮:

  • 如果String變數被宣告為final時就已經被賦值,則它被編譯器自動處理為常量,因而它就會被當作常量池的變數來處理。
public class ConstantPool {
    public static final String s1 = "ab";
    public static final String s2 = "cd";
    
    public static void main(String... args) {
        String s = s1 + s2;
        String ss = "abcd";
        
        System.out.println(s == ss);  // true
    }
}
  • 而如果宣告為final的字串沒有在宣告時被賦值,則編譯器無法事先決定它的準確值,所以依舊會把它當作一個變數來處理。
public class ConstantPool {
    public static final String s1;
    public static final String s2;
    
    static {
        s1 = "ab";
        s2 = "cd";
    }
    
    public static void main(String... args) {
        String s = s1 + s2;
        String ss = "abcd";
        
        System.out.println(s == ss);  // false
    }
}

3. intern()方法

String.intern()方法,可以在runtime期間將常量加入到常量池(constant pool)。它的運作方式是:

  1. 如果constant pool中存在一個常量恰好等於這個字串的值,則inter()方法返回這個存在於constant pool中的常量的引用。
  2. 如果constant pool不存在常量恰好等於這個字串的值,則在constant pool中建立一個新的常量,並將這個字串的值賦予這個新建立的在constant pool中的常量。intern()方法返回這個新建立的常量的引用。

示例:

public static void main(String... args) {
    String s1 = "abcd";
    String s2 = new String("abcd");

    /**
     * s2.intern() will first search String constant pool,
     * of which the value is the same as s2.
     */
    String s3 = s2.intern();
    // As s1 comes from constant pool, and s3 is also comes from constant pool, they`re same.
    System.out.println(s1 == s3);
    // As s2 comes from heap but s3 comes from constant pool, they`re different.
    System.out.println(s2 == s3); 
}

/**
 * Output:
 *     true
 *     false
 */

回顧到最開始的第一部分,為什麼要引入intern()這個函式呢?就是因為,雖然"abcd"是被分配在constant pool裡的,但是,一旦使用new String("abcd")就會在heap中新建立一個值為abcd的物件出來。試想,如果有100個這樣的語句,豈不是就要在heap裡建立100個同樣值的物件?!這就造成了執行的低效和空間的浪費。

於是,如果引入了intern()它就會直接去constant pool找尋是否有值相同的String物件,這就極大地節省了空間也提高了執行效率。

相關文章