String 为什么不可变?常量池与字符串拼接是怎样的?
结论先行:String 用 final 修饰类与存储字段且不暴露任何修改入口,因此不可变,带来常量池共享安全、hashCode 可缓存、天然线程安全等收益。字符串常量池在 HotSpot 中自 JDK 7 起位于堆内并缓存字面量;+ 拼接在编译期做常量折叠,运行期则通过 StringBuilder 完成。
要点梳理
| 问题 | 答案 |
|---|---|
| 为何不可变 | final 类 + final 字符数组 + 无修改方法,所有“修改”都返回新对象 |
| 收益 | 常量池共享安全、hashCode 可缓存、天然线程安全、适合做 key |
| 常量池位置 | JDK 7 起移入堆;intern() 可把运行期字符串手动入池 |
| "a" + "b" | 编译期直接折叠为 "ab"(常量折叠) |
| 变量参与拼接 | 编译为 new StringBuilder().append(...),循环拼接注意复用 |
面试必答的 == 陷阱
- 字面量 "ab" 在常量池只有一份;new String("ab") 在堆上另造对象,池中仍有副本。
- 两个 final 常量拼接在编译期折叠,结果是池中引用;变量拼接在运行期产生新对象。
- 判断相等一律用 equals;== 只表达“是不是同一个对象”,别被 intern 演示带偏。
public class StringDemo {
public static void main(String[] args) {
String a = "ab"; // 常量池引用
String b = new String("ab"); // 堆上新对象
System.out.println(a == b); // false:池中 vs 堆上
System.out.println(a == b.intern()); // true:intern 返回池中引用
String s1 = "a" + "b"; // 编译期折叠 → 池中 "ab"
String s2 = "a";
String s3 = s2 + "b"; // 运行期 StringBuilder 拼接 → 新对象
System.out.println(a == s1); // true
System.out.println(a == s3); // false
}
}
常见追问 / 记忆点
- 记忆点:字面量 vs 堆对象、== 比地址 vs equals 比内容、拼接看是否常量折叠。
- 追问:循环内做字符串拼接要显式使用 StringBuilder,避免每轮循环都 new。
- 追问:StringBuffer 是线程安全版(方法级 synchronized),性能弱于 StringBuilder,单线程用后者。
- 追问:JDK 9+ 把 String 内部从 char[] 改为 byte[] + coder 标记,纯 Latin-1 内容内存占用直接减半。