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。
maybeLatin1 是 delete/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;delete的end越界静默截断。两处都是宽松语义换易用性,也都是 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。