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 内容内存占用直接减半。
笔记加载中…