String、StringBuilder、StringBuffer 区别

面试官问:"String、StringBuilder、StringBuffer 有什么区别?"

候选人小陈答:"String 不可变,StringBuilder 可变线程不安全,StringBuffer 可变线程安全。"

面试官点点头:"那在一个循环里拼接字符串,用 String 会有什么问题?"

小陈:"会产生很多中间对象,性能差。"

面试官追问:"JVM 会对字符串拼接做优化吗?具体是什么优化?"

小陈愣了三秒:"...会自动换成 StringBuilder?"

面试官:"JDK 9 之后呢?"

小陈彻底卡住了。

【面试官心理】 这道题 90% 的候选人能说出三个关键词,但能说出"JVM 优化细节"的只有 10%。问 JDK 9 的变化,是在筛选有没有关注过 Java 版本演进的候选人。

一、核心区别 🔴

维度StringStringBuilderStringBuffer
可变性不可变(final char[]可变可变
线程安全安全(不可变天然安全)不安全安全(synchronized)
性能拼接慢(每次新建对象)最快慢于 StringBuilder
适用场景字符串常量、少量操作单线程字符串拼接多线程字符串拼接(少见)

二、String 的不可变性 🔴

2.1 底层实现

// JDK 8 及之前:char 数组
public final class String {
    private final char[] value; // final:引用不可变
    // ...
}

// JDK 9 及之后:byte 数组(优化内存)
public final class String {
    private final byte[] value; // Latin-1 只用 1 byte 存储
    private final byte coder;   // 编码标识:LATIN1 或 UTF16
    // ...
}

2.2 String 的"修改"本质是新建

String s = "hello";
s = s + " world"; // 不是修改,而是新建了 "hello world",s 指向新对象
// 旧的 "hello" 成为垃圾,等待 GC

2.3 为什么 String 要设计成不可变

  1. 线程安全:不可变对象可以安全共享,无需同步
  2. 字符串常量池:相同字面量共享同一对象,节省内存
  3. hashCode 可缓存:hashCode 只计算一次,后续缓存复用(HashMap 性能优化)
  4. 安全性:网络连接、文件路径等场景,String 不会被意外修改

三、StringBuilder 的底层实现 🔴

3.1 动态数组扩容

public final class StringBuilder extends AbstractStringBuilder {
    // 继承 AbstractStringBuilder
}

abstract class AbstractStringBuilder {
    char[] value; // 可变字符数组
    int count;    // 当前字符数量

    public AbstractStringBuilder append(String str) {
        // 1. 检查容量是否足够
        ensureCapacityInternal(count + str.length());
        // 2. 复制字符
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0) {
            // 扩容:新容量 = max(原容量*2+2, 需要的最小容量)
            value = Arrays.copyOf(value, newCapacity(minimumCapacity));
        }
    }
}

默认初始容量 16,扩容公式:新容量 = 旧容量 * 2 + 2

💡

如果提前知道最终字符串长度,用 new StringBuilder(expectedLength) 预设容量,避免扩容带来的数组复制开销。这是生产中的优化技巧。

3.2 StringBuffer 的线程安全

public final class StringBuffer extends AbstractStringBuilder {
    // 几乎所有方法都加了 synchronized
    @Override
    public synchronized StringBuffer append(String str) {
        // ...
    }
}

synchronized 保证了线程安全,但也带来了锁开销,性能比 StringBuilder 差。

四、字符串拼接的 JVM 优化 🔴

4.1 JDK 8 的优化:编译期替换

// 源代码
String result = "hello" + " " + "world";
// 编译期优化(javac 直接合并常量)→ "hello world"

// 源代码
String a = "hello";
String b = " world";
String result = a + b;
// 编译期优化:替换为 StringBuilder
// 等价于:new StringBuilder().append(a).append(b).toString()

4.2 循环中拼接的陷阱

// ❌ 错误:每次循环都新建 StringBuilder,编译期优化无效
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i; // 等价于 result = new StringBuilder(result).append(i).toString()
    // 每次循环新建 StringBuilder 和 String,产生大量垃圾对象
}

// ✅ 正确:复用同一个 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();
⚠️

循环中用 += 拼接字符串,即使 JVM 会做优化,每次循环的优化也是独立的(每次都 new 一个 StringBuilder),并不能跨循环复用。这是高频考点,也是生产中最常见的性能问题之一。

4.3 JDK 9 的优化:invokedynamic

JDK 9 引入了基于 invokedynamic 指令的字符串拼接优化:

// JDK 9+ 的字节码不再是 StringBuilder,而是 invokedynamic
// JVM 在运行时动态选择最优的拼接策略
// 可以利用 StringConcatFactory 进行更高效的内存分配

优点:

  • 不再固定使用 StringBuilder,JVM 可以根据情况选择更优实现
  • 预分配正确大小的 byte 数组,减少内存分配
💡

JDK 9 的 invokedynamic 字符串拼接优化,是面试 P6/P7 的加分点。能说出这个,面试官会认为候选人关注了 Java 版本演进细节。

五、追问升级链

第一层:三者区别?
→ 可变性、线程安全、性能

第二层:String 不可变的底层实现和原因?
final char[]/final byte[],线程安全/常量池/hashCode 缓存

第三层:字符串拼接的编译器优化?
→ 常量折叠、StringBuilder 替换(JDK 8),invokedynamic(JDK 9+)

第四层:什么场景必须用 StringBuffer?
→ 多线程共享同一个 StringBuilder 时。但实际生产中,更常见的方案是避免多线程共享可变字符串(用 ThreadLocal 或局部变量)。

六、选型总结

单线程字符串拼接(绝大多数场景):StringBuilder
多线程共享字符串构建(极少见):StringBuffer
字符串常量/少量操作:String
💡

实际生产中 StringBuffer 几乎不用,因为多线程场景下通常直接用局部变量(每个线程自己的 StringBuilder),不存在共享问题。能说出这个工程判断的候选人,面试官会认为有生产经验。