1. String 为什么是不可变的?不可变有什么好处?

不可变的三重保证:

好处:

  1. 线程安全:不可变对象天然安全,无需加锁
  2. 常量池缓存:值相同的 String 可复用同一对象,节省内存(HashMap key 不被误改,安全性同理)
  3. 适合做 HashMap 的 key:hashCode 只算一次且永不失效
  4. 安全性:网络地址、类名、文件路径等场景不被恶意篡改

🎯 面试要点

  • JDK9+ String 用 byte[] + coder 字段,Latin-1 字符只占 1 字节(紧凑字符串),内存减半
  • 任何看似"修改"的操作都是生成新对象,频繁修改会大量产生垃圾 → 用 StringBuilder

2. String、StringBuilder、StringBuffer 的区别?

特性 String StringBuilder StringBuffer
可变性不可变可变可变
线程安全安全(不可变)不安全安全(方法 synchronized)
性能拼接慢(建新对象)最快较慢(加锁开销)
适用值固定/做 key单线程循环拼接多线程共享拼接(极少)

StringBuffer 的加锁在现代业务中几乎无用武之地:方法内局部变量天然线程私有,直接用 StringBuilder;真正的跨线程共享用不可变 String 或消息队列。

🎯 面试要点

  • 底层实现相同:都是 char[]/byte[],StringBuffer 只是方法上加了 synchronized
  • StringBuilder 初始容量 16,append 扩容策略:newCap = old * 2 + 2(超过则取实际长度)
  • 循环里拼字符串必须用 StringBuilder,否则每次 + 都生成中间 String

3. 字符串常量池、intern() 与 == 比较(经典面试题)

字符串常量池(JDK7+ 移入堆):保存字符串字面量和 intern 的字符串。相同内容的字面量只存一份。

两种创建方式的区别:

intern():把当前字符串内容放入常量池,若已存在则返回池中对象。

运行结果判断
String s1 = "hello";
String s2 = new String("hello");
String s3 = s2.intern();

System.out.println(s1 == s2);   // false:常量池 vs 堆
System.out.println(s1 == s3);   // true:intern 返回池中对象

// 拼接:字面量 + 字面量 → 编译期折叠,还是常量池对象
String s4 = "he" + "llo";
System.out.println(s1 == s4);   // true(编译期常量折叠)

// 拼接:含变量的表达式 → 运行期 new,堆中新建
String s5 = "he" + new String("llo");
System.out.println(s1 == s5);   // false

🎯 面试要点

  • JDK7 前常量池在方法区(永久代);JDK7 起移入堆中
  • 字符串内容比较永远用 equals / Objects.equals,绝不用 ==(除非比较的是 intern 过的对象)
  • 大量重复字符串场景用 intern 可去重省内存,但 intern 操作本身有性能开销

4. "+"拼接字符串的性能真相

编译期优化:

循环拼接的两种写法对比
// ❌ 每轮循环 new StringBuilder,O(n²) 复制开销
String s = "";
for (int i = 0; i < 10000; i++) {
    s += i;
}

// ✅ 复用同一个 StringBuilder,O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String s2 = sb.toString();

🎯 面试要点

  • JDK9 起 javac 用 invokedynamic(StringConcatFactory)替代了 StringBuilder 拼接,但循环内问题依旧
  • 大数据量拼接还可以考虑 String.join()(内部用 StringBuilder)或 Stream.collect(joining())

🎤 常见面试追问

  1. String 为什么设计成不可变?——① 线程安全(不可变天然安全);② 常量池可以缓存复用同一对象(可变的话缓存就没法共享);③ HashMap 的 key 安全(不被误改);④ 安全性(类名/路径不被篡改)。
  2. 字符串常量池在 JDK7 前后有什么变化?——JDK7 前在方法区(永久代);JDK7 起移到堆中——好处:常量池随堆 GC 管理,不易 OOM。
  3. intern() 的坑是什么?——大量调用 intern 会占用常量池空间(JDK7 后在堆里,可能引发 GC 压力);只有字符串大量重复时才值得。
  4. "a" + "b" 和变量拼接有什么区别?——纯字面量拼接编译期就折叠成 "ab"(常量池);含变量的拼接运行期创建新对象(编译成 StringBuilder 或 invokedynamic)。
  5. 为什么 String 拼接不要用 + 在循环里?——循环每次 + 都 new StringBuilder + new String,产生大量垃圾对象,O(n²) 复制开销;复用 StringBuilder 是 O(n)。

📖 名词解释(本页术语)

术语 大白话解释
String不可变的字符串类。内容一旦创建不能改,所有"修改"方法都返回新对象。
字符串常量池存放字符串字面量的"仓库"(JDK7 起在堆里):相同内容的字面量只存一份,复用省内存。new String("abc") 会额外在堆里再建一份。
intern()把字符串内容放进常量池的方法:已存在返回池中对象,不存在则创建。用于省内存/比较引用。
StringBuilder可变的字符串容器,append 拼接效率高,单线程用它。循环拼接的标配。
StringBufferStringBuilder 的线程安全版(方法加 synchronized),多线程共享时才需要,平时用 StringBuilder。
equals vs ==== 比较引用(是不是同一个对象);equals 比较内容(String 重写过)。字符串比较永远用 equals。
编译期常量折叠"he" + "llo" 在编译时就拼成 "hello" 存进常量池,运行期不创建新对象。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。