String 和它的 intern
先做道題
Q1:定義了幾個對象?
public class Q1 {
public static void main(String[] args) {
String s = new String("HelloWorld"); // 2 個(堆中 String 對象,常量池中字符串 "HelloWorld" 對象)
}
}
Q2:判斷下面代碼輸出
public class Q2 {
public static void main(String[] args) {
String s1 = "HelloWorld";
String s2 = "Hello" + "World";
String s3 = new String("HelloWorld");
String s4 = s3.intern();
System.out.println(s1 == s2); // true,都指向常量池中字符串 "HelloWorld" 對象
System.out.println(s1 == s3); // false,符號引用 s3 指向堆中 String 對象
System.out.println(s1 == s4); // true,都指向常量池中字符串 "HelloWorld" 對象
}
}
一、String 字符串
String 表示字符串,不屬於基本類型,但它可以像基本類型一樣,直接通過字面量賦值
編譯期
在編譯期,符號引用和字面量會被加入到 Class 文件的常量池中,然後在類加載階段,會進入常量池(常量池中已存在的字符串字面量不會重複添加)
常量池
Java 在內存分配中有 3種常量池:Class 常量池(靜態常量池)、字符串常量池(存在於運行時常量池)、運行時常量池
- 常量池屬於線程共享數據區
- 常量池可分爲兩大類:靜態常量池(Class 常量池,存在於 Class 文件中)和運行時常量池
- 運行時常量池是方法區的一部分,存放一些運行時常量數據
字符串常量池
字符串常量池存在運行時常量池之中(JDK 6 及以前,存在方法區中;從 JDK 7 開始,存在於堆中)
二、intern() 方法(JDK 1.8)
/**
* ...
* When the intern method is invoked, if the pool already contains a
* string equal to this String object as determined by
* the equals(Object) method, then the string from the pool is
* returned. Otherwise, this String object is added to the
* pool and a reference to this String object is returned.
* 調用 intern 方法時,如果池中已包含由 equals 方法確定的與此 string 對象相等的字符串,則返回池中的字符串
* 否則,此字符串對象將添加到池中,並返回對此字符串對象的引用
* ...
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
* 返回 一個字符串,其內容與此字符串相同,但保證來自唯一字符串池
*/
public native String intern();
上面 String#intern() 方法的註釋中,其中一段所要表達的意思很明確:“調用 intern 方法時,如果池中已包含由 equals 方法確定的與此 string 對象相等的字符串,則返回池中的字符串。否則,此字符串對象將添加到池中,並返回對此字符串對象的引用”,就是說當 String 實例調用 intern() 方法時,JVM 會在字符串常量池中查詢當前字符串是否存在,若存在,返回當前字符串;若不存在,則先將當前字符串放入常量池中,之後再返回
在 JDK 6 及以前版本中,字符串常量池裏放的都是字符串常量;從 JDK 7 開始,字符串常量池從方法區移到了堆中,字符串常量池中也可以存放放於堆內的字符串對象的引用
三、字符串分析(JDK 1.8)
先用雙引號創建字符串,再用一個 new 創建字符串
public class Test1 {
public static void main(String[] args) {
String str1 = "Test1";
String str2 = new String("Test1");
String str3 = str2.intern();
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // true
System.out.println(str2 == str3); // false
}
}
String str1 = “Test1”;
編譯期:將字面量 Test1 會被加入到 Class 文件的常量池中
類加載階段:字面量 Test1 會進入字符串常量池(注意:進入的條件是該常量此時不在字符串常量池,即不會重複添加相同常量)
str1 指向字符串常量池中字符串 “Test1” 對象
String str2 = new String(“Test1”);
編譯期:在此之前字符串常量池中已存在 “Test1”,不會重複添加到字符串常量池
運行期:在堆中創建一個 String 對象
符號引用 str2 指向堆中創建的 String 對象
String str3 = str2.intern();
字符串常量池中已存在 “Test1”,調用 intern() 方法,不會重複添加到字符串常量池,直接返回字符串常量池中 “Test1” 對象
str3 指向字符串常量池中 “Test1” 對象
因此
str1(字符串常量池中字符串 “Test1” 對象)== str2(堆中創建的 String 對象):false
str1(字符串常量池中字符串 “Test1” 對象)== str3(字符串常量池中字符串 “Test1” 對象):true
str2(堆中創建的 String 對象)== str3(字符串常量池中字符串 “Test1” 對象):false
小結:
- 雙引號創建的字符串在字符串常量池中
- 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 調用前後兩個符號引用指向的不是同一個對象
JVM 啓動過程中加入常量池的字符串
上面的字符串 “Test1” 在字符串常量池中一開始並沒有,但有的字符串在 JVM 啓動過程中就已經存在,如:“Java”
public class Test2 {
public static void main(String[] args) {
String str1 = new String("ja") + new String("va");
String str2 = str1.intern();
String str3 = "java";
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // false
System.out.println(str2 == str3); // true
}
}
String str1 = new String(“ja”) + new String(“va”);
編譯期:將字面量 ja 和 va 會被加入到 Class 文件的常量池中
類加載階段:字面量 ja 和 va 會進入字符串常量池
運行期:在堆中創建三個 String 對象(兩個匿名 String 對象,一個符號引號 str1 指向的 String 對象)
str1 指向堆中創建的 String 對象
String str2 = str1.intern();
編譯期:在此之前字符串常量池中已存在 “java”,不會重複添加到字符串常量池(在 JVM 啓動的時候會調用了一些方法,在常量池中會生成 “java” 等字符串常量)
str2 指向字符串常量池中的字符串 “java” 對象
String str3 = “java”;
編譯期:在此之前字符串常量池中已存在 “Test1”,不會重複添加到字符串常量池
str3 指向字符串常量池中的字符串 “java” 對象
因此
str1(堆中創建的 String 對象)== str2(字符串常量池中的字符串 “java” 對象):false
str1(堆中創建的 String 對象)== str3(字符串常量池中的字符串 “java” 對象):false
str2(字符串常量池中的字符串 “java” 對象)== str3(字符串常量池中的字符串 “java” 對象):true
小結:
- 特殊情況:在 JVM 啓動的時候會調用了一些方法,在常量池中會生成 “java” 等字符串常量(正常情況:下方 “### 先用兩個 new 創建字符串”)
- JVM 啓動過程中加入常量池的字符串,調用 intern() 方法,指向字符串常量池中的字符串對象
先用一個 new 創建字符串
public class Test3 {
public static void main(String[] args) {
// 情況一
String s1 = new String("Test3_1");
String s2 = s1.intern(); // intern() 在雙引號之前調用
String s3 = "Test3_1";
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // false
System.out.println(s2 == s3); // true
System.out.println();
// 情況二
String s4 = new String("Test3_2");
String s5 = "Test3_2";
String s6 = s4.intern(); // intern() 在雙引號之後調用
System.out.println(s4 == s5); // false
System.out.println(s4 == s6); // false
System.out.println(s5 == s6); // true
}
}
情況一
符號引號 s1 指向堆中創建的 String 對象(字符串內容是 “Test3_1”)
符號引用 s2 指向字符串常量池中的字符串 “Test3_1” 對象
符號引用 s3 指向字符串常量池中的字符串 “Test3_1” 對象
情況二
符號引號 s4 指向堆中創建的 String 對象(字符串內容是 “Test3_2”)
符號引用 s5 指向字符串常量池中的字符串 “Test3_2” 對象
符號引用 s6 指向字符串常量池中的字符串 “Test3_2” 對象
因此
s1(堆中創建的 String 對象,字符串內容是 “Test3_1”)== s2(字符串常量池中的字符串 “Test3_1” 對象):fasle
s1(堆中創建的 String 對象,字符串內容是 “Test3_1”)== s3(字符串常量池中的字符串 “Test3_1” 對象):fasle
s2(字符串常量池中的字符串 “Test3_1” 對象)== s3(字符串常量池中的字符串 “Test3_1” 對象):true
s4(堆中創建的 String 對象,字符串內容是 “Test3_2”)== s5(字符串常量池中的字符串 “Test3_2” 對象):fasle
s4(堆中創建的 String 對象,字符串內容是 “Test3_2”)== s6(字符串常量池中的字符串 “Test3_2” 對象):fasle
s5(字符串常量池中的字符串 “Test3_2” 對象)== s6(字符串常量池中的字符串 “Test3_2” 對象):true
小結:
- 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 前後兩個符號引用指向的不是同一個對象
先用兩個 new 創建字符串
public class Test4 {
public static void main(String[] args) {
// 情況一
String s1 = new String("Test4_1") + new String("Test4_1");
String s2 = s1.intern(); // intern() 在雙引號之前調用
String s3 = "Test4_1Test4_1";
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s2 == s3); // true
System.out.println();
// 情況二
String s4 = new String("Test4_2") + new String("Test4_2");
String s5 = "Test4_2Test4_2";
String s6 = s4.intern(); // intern() 在雙引號之後調用
System.out.println(s4 == s5); // false
System.out.println(s4 == s6); // false
System.out.println(s5 == s6); // true
}
}
情況一
符號引號 s1 指向堆中創建的 String 對象(字符串內容是 “Test4_1Test4_1”)
符號引用 s2 指向字符串常量池中的引用,該引用指向 s1 引用的對象
符號引用 s3 指向字符串常量池中的引用,該引用指向 s1 引用的對象
情況二
符號引號 s4 指向堆中創建的 String 對象(字符串內容是 “Test4_2Test4_2”)
符號引用 s5 指向字符串常量池中的字符串 “Test4_2Test4_2” 對象
符號引用 s6 指向字符串常量池中的字符串 “Test4_2Test4_2” 對象
因此
s1(堆中創建的 String 對象,字符串內容是 “Test4_1Test4_1”)== s2(字符串常量池中的一個引用,該引用指向 s1 引用的對象):true
s1(堆中創建的 String 對象,字符串內容是 “Test4_1Test4_1”)== s3(字符串常量池中的一個引用,該引用指向 s1 引用的對象):true
s2(字符串常量池中的一個指向 s1 的引用)== s3(字符串常量池中的一個指向 s1 的引用):true
s4(堆中創建的 String 對象,字符串內容是 “Test4_2Test4_2”)== s5(字符串常量池中的字符串 “Test4_2Test4_2” 對象):fasle
s4(堆中創建的 String 對象,字符串內容是 “Test4_2Test4_2”)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 對象):fasle
s5(字符串常量池中的字符串 “Test4_2Test4_2” 對象)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 對象):true
小結:
- JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用前,常量池中不存在拼接後的字符串
- JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用後,常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用(String#intern 方法時,如果存在堆中的對象,會直接保存對象的引用,而不會重新創建對象)
四、總結
- JDK 1.7 及以後,字符串常量池從方法區移動到了堆中
- 雙引號創建的字符串在字符串常量池中
- 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 調用前後兩個符號引用指向的不是同一個對象(調用 intern() 前後分別是:堆中 String 對象、字符串常量池中對應字符串對象)
- 特殊情況:在 JVM 啓動的時候會調用了一些方法,在常量池中會生成 “java” 等字符串常量
- JVM 啓動過程中加入常量池的字符串,調用 intern() 方法,指向字符串常量池中的字符串對象
- 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 前後兩個符號引用指向的不是同一個對象
- JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用前,常量池中不存在拼接後的字符串
- JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用後,常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用(調用 String#intern() 方法時,如果存在堆中的對象,會直接在字符串常量池中保存堆中對象的引用,而不會重新創建字符串對象)