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)。它的運作方式是:
- 如果constant pool中存在一個常量恰好等於這個字串的值,則
inter()
方法返回這個存在於constant pool中的常量的引用。 - 如果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物件,這就極大地節省了空間也提高了執行效率。