String、StringBuilder 和 StringBuffer 的区别?

#Java#面试八股

String、StringBuilder 和 StringBuffer 的区别?

结论:三者都实现 CharSequence,真正的差别只有两点——可变性(mutability)与线程安全(thread-safe)。String 不可变,任何"修改"都返回新对象;StringBuilder 可变、不加锁、单线程最快;StringBuffer 可变、公开方法带 synchronized,线程安全但每次操作都有锁开销。选型口诀:常量和少量拼接用 String,单线程循环拼接用 StringBuilder,多线程共享同一缓冲区才考虑 StringBuffer(这种场景现在很少见)。

1. 一张表看清差别

维度StringStringBuilderStringBuffer
可变性不可变(immutable)可变可变
线程安全安全(对象不可变,天然共享)不安全安全(方法级 synchronized)
拼接代价每次产生新对象原地 append,均摊 O(1)原地 append + 加锁
适用场景常量、缓存 key、少量拼接单线程大量拼接多线程共享同一缓冲区
首次出现JDK 1.0JDK 1.5JDK 1.0
直接父类ObjectAbstractStringBuilderAbstractStringBuilder

关键点:StringBuilder 与 StringBuffer 都继承 AbstractStringBuilder,底层数组与扩容逻辑完全一致,后者只是在对外方法上加了锁,所以两者性能差距主要来自锁的获取与释放。

2. 内部结构:为什么可变对象快

String:          [ final byte[] value ][ coder ][ hash ]      ← 内容不可改
StringBuilder:   [ byte[] value ][ count ][ coder ]
                   ├── 0 ~ count-1 已写入
                   └── count ~ capacity-1 空闲,append 直接写这里

容量不足时:newCapacity = (oldCapacity << 1) + 2,Arrays.copyOf 搬到新数组

JDK 9 引入紧凑字符串(compact strings)后,String 与 AbstractStringBuilder 内部都改成 byte[] value 加一个 byte coder:纯 Latin-1 内容每字符占 1 字节,含中文等字符时用 UTF-16 每字符 2 字节。这也解释了为什么"同样长度的英文串占用的内存比 JDK 8 少一半"。

append 之所以快,是因为它只做两件事:检查剩余容量、把字符写进 value[count++],不创建新对象(除非扩容)。而 String 的 + 每次都要分配一个新数组并复制全部内容,循环里用 += 就会退化成 O(n²)。

3. 代码:扩容与循环拼接

public class BuilderDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();      // 默认容量 16
        sb.append("abc");
        System.out.println(sb.length());             // 3
        System.out.println(sb.capacity());           // 16

        StringBuilder big = new StringBuilder("hello"); // 容量 = 5 + 16 = 21
        big.append(" world, java interview");           // 需要 24 > 21
        System.out.println(big.capacity());             // (21 << 1) + 2 = 44

        // 反例:循环里用 + 拼接,每轮至少新建一个 StringBuilder 和一个结果 String
        String s = "";
        for (int i = 0; i < 10_000; i++) {
            s += i;
        }

        // 正例:复用同一个缓冲区,只在容量不足时扩容
        StringBuilder buf = new StringBuilder(16 * 1024);  // 预估容量,避免反复扩容
        for (int i = 0; i < 10_000; i++) {
            buf.append(i);
        }
        String result = buf.toString();                  // 这里才产生最终 String
    }
}

预估容量是最容易被忽略的优化:一次分配够,胜过十次翻倍复制。反过来说,new StringBuilder() 在拼接结果较大时会经历 16 → 34 → 70 → 142 这样的多次扩容,每次都复制一遍旧内容。

4. 常见追问

追问回答要点
StringBuilder 线程不安全具体表现是什么?count++ 与数组写入不是原子操作,并发 append 会丢字符、内容错乱,极端情况下数组越界
那用 StringBuffer 代替就行吗?不行。正确做法是每个线程一个 StringBuilder;StringBuffer 只能保证单次方法调用原子,复合操作仍要外部加锁
StringBuffer 真的线程安全吗?单方法安全,"先判断再拼接"这类复合逻辑不安全,仍需要 synchronized
toString() 有开销吗?有,返回一份新的不可变副本,O(n) 拷贝;不要在循环里反复 toString() 比较
JDK 9 之后 + 还用 StringBuilder 吗?不用,改成 invokedynamic + StringConcatFactory,一次估算长度直接生成结果,但循环里仍然每轮新建
capacity 和 length 什么区别?length 是已写入字符数,capacity 是数组容量,capacity >= length

还有两个容易答错的细节:

  1. StringBuilder 没有重写 equals/hashCode,比较两个 StringBuilder 内容是否相同必须 toString().equals(...),直接用 == 比的是引用。
  2. StringBuilder 可以 setLength(0) 复用缓冲区,比反复 new 更省;但复用时要注意别把旧数据带进新结果。

小结

String 用不可变换来常量池共享与线程安全,StringBuilder 用可变数组换拼接性能,StringBuffer 用锁换线程安全。面试时别只背"一个可变一个不可变"——要能说出扩容公式、+ 的编译产物、以及为什么 StringBuffer 在单线程下不划算。

笔记加载中…