final class 對象,默認值 nullnew String() 和 new String("") 都是空串 "",長度 0,非 null常量池:編譯期確定、存儲在 .class 文件中的字面量與符號引用。運行期由 JVM 載入方法區,可動態擴充(String.intern())。
// 案例 1:純字面量
String s0 = "kvill";
String s1 = "kvill";
String s2 = "kv" + "ill";
System.out.println(s0 == s1); // true
System.out.println(s0 == s2); // true
// 解析:編譯期常量折疊,s2 直接解析為 "kvill",三者指向常量池同一引用
// 案例 2:混合 new String
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = "kv" + new String("ill");
System.out.println(s0 == s1); // false
System.out.println(s0 == s2); // false
System.out.println(s1 == s2); // false
// 解析:包含 new String() 的表達式運行期創建,堆上新對象,非常量池引用
| 表達式類型 | 創建時機 | 存儲位置 | == 比較行為 |
|---|---|---|---|
| 純字面量 / 常量拼接 | 編譯期 | 常量池 | 同值共用引用 → true |
| 含 new String() | 運行期 | 堆內存 | 每次新建對象 → false |
查找常量池中是否存在內容相同的字符串:
• 存在 → 返回常量池引用
• 不存在 → 在常量池中創建 該字符串並返回引用
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = new String("kvill");
System.out.println(s0 == s1); // false
System.out.println("----------");
s1.intern(); // 返回常量池引用,但未賦值給 s1
s2 = s2.intern(); // 將常量池引用賦給 s2
System.out.println(s0 == s1); // false (s1 仍指向堆對象)
System.out.println(s0 == s1.intern()); // true (intern() 返回常量池引用)
System.out.println(s0 == s2); // true (s2 已指向常量池)
// 反證:intern() 不會將「堆對象地址」登記到常量池
String s1 = new String("kvill"); // 假設常量池無 "kvill"
String s2 = s1.intern(); // 常量池新建 "kvill",返回其引用
System.out.println(s1 == s1.intern()); // false!原堆對象仍存在
System.out.println(s1 + " " + s2); // kvill kvill
System.out.println(s2 == s1.intern()); // true
// 結論:intern() 是「常量池去重」,不改變原堆對象
錯誤說法:「若常量池無該值,則將自己的地址註冊到常量池」
正確理解:常量池創建新字符串對象(內容相同),返回新對象引用。原堆對象完全不受影響,仍由變量引用時存活,無引用時 GC 回收。
String s = "value"; 自動複用常量池intern() 需全局鎖查找/插入,高併發可能成瓶頸== 比較引用,equals() 比較內容| 版本 | 常量池位置 | 影響 |
|---|---|---|
| JDK 6 | PermGen (永久代) | 大量 intern() 易導致 PermGen space OOM |
| JDK 7 | Heap (堆內存) | 受 Heap 大小限制,較難 OOM,但會增加 GC 壓力 |
| JDK 8+ | Metaspace (元空間,Native Memory) | 類元數據移出堆,字符串常量池仍在堆 |
參考資料:
• String 池化及 intern 方法的作用
• 常量池是什麼?String.intern() 什麼時候用?怎麼用?