StringBuilder

StringBuilder / StringBuffer 源码分析

String 不可变,每拼一次就要整份拷贝一次,循环拼接是 O(n2);StringBuilder 在同一块 byte[] 上原地追加、按倍数扩容,把 n 次追加均摊成 O(n)。
StringBuffer 是它加了 synchronized 的兄弟,两者的实现全在共同父类 AbstractStringBuilder 里。
JDK 9 紧凑字符串之后,内部和 String 一样是 byte[] + coder 双编码,本地源码(JDK 25)里这套编码切换逻辑比老版本教程里的复杂得多。

// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.AbstractStringBuilder
abstract sealed class AbstractStringBuilder implements Appendable, CharSequence
    permits StringBuilder, StringBuffer {       // sealed: 只许这两个子类, 真正的实现全在本类

    byte[] value;           // 字符存储。紧凑编码: LATIN1 一字符一字节, UTF16 一字符两字节

    byte coder;             // 当前编码 LATIN1(0)/UTF16(1)。字符容量 = value.length >> coder

    boolean maybeLatin1;    // 删过字符的 UTF16 数组可能已退回纯 Latin1。允许误报, 真正压缩推迟到 toString

    int count;              // 已用字符数(不是字节数), 别和容量 value.length 混淆

    private static final byte[] EMPTYVALUE = new byte[0];

    // ... 省略无参构造(序列化用, value = EMPTYVALUE)

    AbstractStringBuilder(int capacity) {
        if (COMPACT_STRINGS) {                  // 紧凑字符串默认开启: 乐观假设内容纯 Latin1, 内存省一半
            value = new byte[capacity];
            coder = LATIN1;
        } else {
            value = StringUTF16.newBytesFor(capacity);
            coder = UTF16;
        }
    }

    // ... 省略 String/CharSequence 版构造(初始容量 = 参数长度 + 16)

    public AbstractStringBuilder append(String str) {
        if (str == null) {
            return appendNull();                // 规范要求追加 "null" 四个字符而非抛 NPE —— 结果里混进 "null" 的元凶
        }
        byte coder = this.coder;
        int count = this.count;
        byte[] value = this.value;
        int len = str.length();
        byte newCoder = (byte)(coder | str.coder());    // LATIN1=0, UTF16=1, 按位或: 任一方是 UTF16 结果就是 UTF16
        if (needsNewBuffer(value, coder, count + len, newCoder)) {
            this.value = value = ensureCapacityNewCoder(value, coder, count, count + len, newCoder);
            this.coder = newCoder;              // 扩容和 Latin1→UTF16 膨胀合并在一次数组分配里完成
        }
        str.getBytes(value, count, newCoder);   // String 把自己的字节直接写进 builder 数组, 同编码时就是一次 arraycopy
        this.count = count + len;
        return this;                            // 返回 this, 链式 .append().append() 的来源
    }

    private AbstractStringBuilder appendNull() {
        byte coder = this.coder;
        int count = this.count;
        int newCount = count + 4;
        byte[] value = ensureCapacitySameCoder(this.value, coder, newCount);
        if (isLatin1(coder))
            StringLatin1.putCharsAt(value, count, 'n', 'u', 'l', 'l');
        else
            StringUTF16.putCharsAt(value, count, 'n', 'u', 'l', 'l');
        this.count = newCount;
        this.value = value;
        return this;
    }

    // ... 省略十几个 append 重载。基本类型不经过中间 String:
    // append(int) 先用 DecimalDigits.stringSize(i) 算出位数, 扩好容后把数字逐位写进数组

    public AbstractStringBuilder delete(int start, int end) {
        int count = this.count;
        if (end > count) {
            end = count;                        // end 越界不报错, 静默截断到末尾 —— 和 String.substring 行为不同
        }
        Preconditions.checkFromToIndex(start, end, count, Preconditions.SIOOBE_FORMATTER);
        int len = end - start;
        if (len > 0) {
            shift(value, coder, count, end, -len);
            this.count = count - len;
            maybeLatin1 = true;                 // 删掉的可能正是仅有的非 Latin1 字符, 只打标记不扫描
        }
        return this;
    }
}
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.AbstractStringBuilder —— 扩容与编码膨胀
private static boolean needsNewBuffer(byte[] value, byte coder, int newCapacity, byte newCoder) {
    long newLength = (long) newCapacity << newCoder;    // 用 long 算目标字节数, 防 int 溢出后误判"够用"
    return coder != newCoder || newLength > value.length || 0 > newLength;
}

// 需要换编码时走这条路: 扩容 + Latin1→UTF16 膨胀一次完成
private static byte[] ensureCapacityNewCoder(byte[] value, byte coder, int count,
                                             int minimumCapacity, byte newCoder) {
    assert coder == newCoder || newCoder == UTF16 : "bad new coder UTF16 -> LATIN1";    // 编码只升不降
    int oldCapacity = value.length >> coder;
    int growth = minimumCapacity - oldCapacity;
    int newCapacity = (growth <= 0)
            ? oldCapacity                       // 容量够就不动, 也绝不缩容
            : newCapacity(value, newCoder, minimumCapacity);
    assert count <= newCapacity : "count exceeds new capacity";

    if (coder == newCoder) {
        if (newCapacity > oldCapacity) {
            value = Arrays.copyOf(value, newCapacity << newCoder);
        }
        return value;
    } else {
        byte[] newValue = StringUTF16.newBytesFor(newCapacity);
        StringLatin1.inflate(value, 0, newValue, 0, count);     // 旧内容一字节膨胀成两字节, 顺手完成扩容
        return newValue;
    }
}

// 编码不变时的普通扩容(ensureCapacity 公开方法底下也是它)
private static byte[] ensureCapacitySameCoder(byte[] value, byte coder, int minimumCapacity) {
    int oldCapacity = value.length >> coder;
    if (minimumCapacity - oldCapacity > 0) {            // 减法比较而非 <, 溢出安全
        value = Arrays.copyOf(value,
                newCapacity(value, coder, minimumCapacity) << coder);
    }
    return value;
}

private static int newCapacity(byte[] value, byte coder, int minCapacity) {
    int oldLength = value.length;
    int newLength = minCapacity << coder;
    int growth = newLength - oldLength;
    int length = ArraysSupport.newLength(oldLength, growth, oldLength + (2 << coder));  // 首选增量 = 旧容量 + 2 字符, 即扩到 2 倍 + 2(不是 ArrayList 的 1.5 倍)
    if (length == Integer.MAX_VALUE) {
        throw new OutOfMemoryError("Required length exceeds implementation limit");
    }
    return length >> coder;
}
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.StringBuilder
public final class StringBuilder
    extends AbstractStringBuilder
    implements Appendable, java.io.Serializable, Comparable<StringBuilder>, CharSequence
{
    // ... 省略 serialVersionUID

    @IntrinsicCandidate                 // JIT 特殊对待: 字符串拼接优化会识别并消除这些调用
    public StringBuilder() {
        super(16);                      // 默认 16 字符
    }

    @IntrinsicCandidate
    public StringBuilder(int capacity) {
        super(capacity);                // 能预估终态长度就用它一次给够, 全程零扩容零拷贝
    }

    // ... 省略几十个 append/insert 覆写: 内容全是 super.xxx(...) 后 return this,
    // 只为把返回类型从 AbstractStringBuilder 收窄成 StringBuilder, 保住链式调用

    @Override
    @IntrinsicCandidate
    public String toString() {
        if (length() == 0) {
            return "";                  // 空串走常量, 不分配
        }
        // Create a copy, don't share the array
        return new String(this, null);  // 必须拷贝: 共享数组的话后续 append 会改写已"生成"的 String
    }
}

原理串讲

走一遍 new StringBuilder().append("ab").append('中').toString()
构造函数在 COMPACT_STRINGS(默认开启)下直接 new byte[16]coder = LATIN1——乐观地假设你只会拼 ASCII。
为什么敢这么赌?因为真实系统里绝大多数字符串(日志、JSON key、SQL、URL)确实是纯 Latin1,一字符一字节能省一半内存、加倍 CPU cache 命中率,赌输的代价则被设计得很低,见下一步。

append("ab") 进入 AbstractStringBuilder.append(String)
第一步算 newCoder = coder | str.coder():LATIN1 是 0、UTF16 是 1,一个按位或就完成了”双方任一是 UTF16,结果就得是 UTF16”的编码合并,没有分支。
接着 needsNewBuffer 判断是否要换数组——编码没变、16 字节也够,直接 str.getBytes(value, count, newCoder) 一次 arraycopy 写入,count 变 2。

append('中') 就赌输了:StringLatin1.coderFromChar('中') 返回 UTF16,newCoder 变 1,与当前 coder 不等,needsNewBuffer 为 true,走 ensureCapacityNewCoder
这个函数把两件事合并在一次分配里:按 newCapacity 算好新容量、StringUTF16.newBytesFor 建 UTF16 数组,再用 StringLatin1.inflate 把已有的 “ab” 逐字节膨胀成双字节搬过去。
为什么要合并?扩容和变码各干各的就要两次分配两次全量拷贝,而两者的本质都是”把旧内容搬进新数组”,一次搬完是纯赚。

容量不够时 newCapacity 委托 ArraysSupport.newLength(oldLength, growth, oldLength + (2 << coder)):首选新容量是旧容量的 2 倍加 2。
翻倍保证 n 次 append 的总拷贝量是 O(n)(均摊 O(1));加 2 是为了让容量 0 的实例(new StringBuilder(0) 或反序列化出的 EMPTYVALUE)也能起步——0 乘 2 永远是 0。

最后 toString()new String(this, null)(String 的包私有构造):Latin1 时直接 Arrays.copyOfRange;UTF16 且 maybeLatin1 为 true 时先 StringUTF16.compress 试着压回 Latin1。
maybeLatin1delete/deleteCharAt/setLength 缩短内容时打的标记——删掉的可能正是仅有的中文字符。
为什么不在 delete 时立刻检查?逐次删除逐次全量扫描太贵,而且 builder 还在变,压了可能马上又要膨胀回去;推迟到 toString 这个”内容定稿”的时刻只花一次钱,标记允许误报也无所谓,compress 失败无非维持 UTF16。
String 不可变的原理见 String

StringBuffer 侧(本地 StringBuffer.java):几乎每个方法都是 synchronized 后调 super,另加一个 private transient String toStringCache——所有修改方法第一行置 null,toString 命中缓存就跳过数组拷贝。
这个缓存只对”改一次、读多次 toString”的场景有意义,代价是每个修改方法多一次写。

设计取舍

  • 扩容 2 倍 + 2,比 ArrayList 的 1.5 倍激进:字符串拼接的终态长度更难预估,宁可多要内存也要少拷贝;能预估就 new StringBuilder(len) 直接绕开整套扩容。
  • append(null) 得到字符串 "null" 而非 NPE;deleteend 越界静默截断。两处都是宽松语义换易用性,也都是 bug 的常见来源。
  • toString() 每次都全量拷贝。循环里反复 sb.toString() 做中间检查,就把 O(n) 又变回了 O(n2)。
  • 循环里 s += x 每轮都要新建串并整份拷贝旧内容(JDK 9 后单次 + 由 StringConcatFactory 的 invokedynamic 优化,很快,但循环累积仍是 O(n2)),这才是必须手动 StringBuilder 的场景。
  • StringBuffer 的 synchronized 只保证单方法原子,check-then-append 这类复合操作照样要外部加锁,所以它几乎总是错误选项:多线程各用各的 StringBuilder,最后合并。
  • StringBuilder 实现了 Comparable 却没重写 equals,自然序与 equals 不一致,别拿它当 SortedMap/SortedSet 的 key。

延伸阅读