AbstractStringBuilder ------ 可变字符串的骨架实现
适用版本: JDK 8 难度等级: 进阶 核心概念: 动态扩容算法、数组拷贝优化、位运算
一、类结构与设计意图
1.1 继承层次
scss
Appendable (接口)
↑
CharSequence (接口)
↑
AbstractStringBuilder (抽象类)
↙ ↘
StringBuffer StringBuilder
(线程安全/慢) (非线程安全/快)
AbstractStringBuilder 是典型的模板方法模式应用:它实现了可变字符序列的公共逻辑(扩容、追加、插入、删除、反转),而将同步策略交给了两个子类各自决定。
1.2 核心属性
java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value; // 字符存储容器
int count; // 已使用的字符数量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
| 属性 | 说明 | 细节 |
|---|---|---|
value |
可变字符数组 | 未使用 final,因为需要扩容重建 |
count |
当前有效字符数 | 小于等于 value.length |
length() |
返回 count |
有效字符的长度 |
capacity() |
返回 value.length |
容器的当前容量 |
MAX_ARRAY_SIZE 定为 Integer.MAX_VALUE - 8 是因为某些 JVM 实现在数组头部存储元数据(如数组长度),需要预留 8 字节。
二、动态扩容算法深度解析
2.1 扩容触发链路
scss
append(str)
→ ensureCapacityInternal(count + str.length())
→ newCapacity(minCapacity)
→ Arrays.copyOf(value, newCapacity)
java
// 步骤1: 公开的扩容入口
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
// 步骤2: 内部判断是否需要扩容
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
// 步骤3: 计算新的容量值
private int newCapacity(int minCapacity) {
int newCapacity = (value.length << 1) + 2; // 原容量 × 2 + 2
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity; // 如果还不够,直接用需求值
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
// 步骤4: 处理超大容量边界
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) {
throw new OutOfMemoryError(); // 溢出保护
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
2.2 扩容公式可视化
ini
newCapacity = oldCapacity × 2 + 2
示例:
初始容量 16 → 扩容到 34 → 70 → 142 → ...
java
public class CapacitySimulation {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder(16);
System.out.println("初始容量: " + sb.capacity());
for (int i = 0; i < 20; i++) {
sb.append("AB"); // 每次追加2个字符
}
// 需求: 16 + 20*2 = 56 → 16*2+2=34 不够 → 34*2+2=70 够了
System.out.println("追加后容量: " + sb.capacity()); // 70
System.out.println("有效长度: " + sb.length()); // 40
}
}
2.3 扩容中的 int 溢出处理
java
public class OverflowDemo {
public static void main(String[] args) {
// 模拟超大容量场景
int hugeValue = Integer.MAX_VALUE - 4;
// newCapacity 计算: (hugeValue << 1) + 2 会溢出为负数
int shifted = hugeValue << 1;
System.out.println("左移后: " + shifted); // -10(溢出)
// 触发 hugeCapacity 分支
int result = (shifted + 2 <= 0)
? Integer.MAX_VALUE - 8
: shifted + 2;
System.out.println("最终容量: " + result); // Integer.MAX_VALUE - 8
}
}
三、核心方法源码分析
3.1 append ------ 从尾部追加
java
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull(); // null → "null"
int len = str.length();
ensureCapacityInternal(count + len); // 1. 确保容量
str.getChars(0, len, value, count); // 2. 复制字符
count += len; // 3. 更新计数
return this; // 4. 返回自身支持链式调用
}
appendNull() 方法是个有趣的设计:当传入 null 时,追加字符串 "null" 而非抛异常。
3.2 insert ------ 从指定位置插入
java
public AbstractStringBuilder insert(int offset, String str) {
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
if (str == null)
str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
// 核心:挪出空位 + 填入新内容
System.arraycopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
count += len;
return this;
}
插入操作的成本是 O(n),因为需要将 offset 后的所有字符向后移动 len 位。
3.3 delete ------ 删除区间
java
public AbstractStringBuilder delete(int start, int end) {
if (start < 0) throw new StringIndexOutOfBoundsException(start);
if (end > count) end = count;
if (start > end) throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) {
System.arraycopy(value, end, value, start, count - end);
count -= len;
}
return this;
}
删除后不会缩容------value.length 保持不变,只是 count 减小。这也是一个常见的性能设计:数组只增不减。
3.4 reverse ------ 反转字符串
java
public AbstractStringBuilder reverse() {
boolean hasSurrogates = false;
int n = count - 1;
for (int j = (n - 1) >> 1; j >= 0; j--) {
int k = n - j;
char cj = value[j];
char ck = value[k];
value[j] = ck;
value[k] = cj;
if (Character.isSurrogate(cj) || Character.isSurrogate(ck)) {
hasSurrogates = true;
}
}
if (hasSurrogates) {
reverseAllValidSurrogatePairs();
}
return this;
}
这里的亮点是对 UTF-16 代理对(Surrogate Pair) 的处理。emoji 等字符占两个 char,反转后需要额外修正位置。
java
public class ReverseSurrogateDemo {
public static void main(String[] args) {
String emoji = " Hello!";
// 占2个char,直接反转会变成乱码
String correct = new StringBuilder(emoji).reverse().toString();
System.out.println(correct); // "!olleH "
// 错误做法:逐char反转会破坏代理对
char[] chars = emoji.toCharArray();
for (int i = 0, j = chars.length - 1; i < j; i++, j--) {
char tmp = chars[i];
chars[i] = chars[j];
chars[j] = tmp;
}
System.out.println(new String(chars)); // 乱码
}
}
四、append 方法的设计模式
4.1 方法重载覆盖全部基本类型
java
public AbstractStringBuilder append(boolean b) // true/false
public AbstractStringBuilder append(char c)
public AbstractStringBuilder append(int i) // Integer.toString
public AbstractStringBuilder append(long l) // Long.toString
public AbstractStringBuilder append(float f) // Float.toString
public AbstractStringBuilder append(double d) // Double.toString
public AbstractStringBuilder append(Object obj) // String.valueOf(obj)
public AbstractStringBuilder append(String str)
public AbstractStringBuilder append(CharSequence s)
public AbstractStringBuilder append(char[] str)
每个基本类型都提供了专属的 append,避免自动装箱的开销。
4.2 链式调用的实现
java
public class ChainingDemo {
public static void main(String[] args) {
String result = new StringBuilder()
.append("ID: ")
.append(1001)
.append(", Name: ")
.append("张三")
.append(", Score: ")
.append(95.5)
.toString();
System.out.println(result);
}
}
每个 append 方法返回 this,使得链式调用成为可能------这是建造者模式的一种应用。
五、System.arraycopy 的深入理解
AbstractStringBuilder 中大量使用了 System.arraycopy,这是 Java 中最快的数组拷贝方式。
java
public static native void arraycopy(
Object src, int srcPos, // 源数组 + 起始位置
Object dest, int destPos, // 目标数组 + 起始位置
int length // 拷贝长度
);
java
public class ArraycopyDemo {
public static void main(String[] args) {
char[] source = {'A', 'B', 'C', 'D', 'E'};
char[] target = new char[10];
// 场景1: 拷贝数组
System.arraycopy(source, 1, target, 2, 3);
// target = [ , , B, C, D, , , , , ]
System.out.println(java.util.Arrays.toString(target));
// 场景2: 同一数组内移动(支持重叠)
char[] self = {'0', '1', '2', '3', '4', '5'};
System.arraycopy(self, 0, self, 2, 3);
// self = [0, 1, 0, 1, 2, 5]
System.out.println(java.util.Arrays.toString(self));
// 场景3: 模拟 delete 操作
char[] data = {'a', 'b', 'c', 'd', 'e'};
int deleteStart = 1, deleteEnd = 3; // 删除 b, c
System.arraycopy(data, deleteEnd, data, deleteStart, 5 - deleteEnd);
System.out.println(java.util.Arrays.toString(data)); // [a, d, e, d, e]
}
}
arraycopy 是 native 方法,底层使用 memmove(而非 memcpy),可以正确处理内存重叠区域。
六、综合实战:自定义高性能日志缓冲器
java
/**
* 基于 AbstractStringBuilder 设计思想的环形日志缓冲器
* 适用于高频写入、批量读取的场景
*/
public class RingLogBuffer {
private char[] buffer;
private int head; // 写入位置
private int tail; // 读取位置
private int size; // 当前存储的字符数
private final int capacity;
public RingLogBuffer(int capacity) {
this.capacity = capacity;
this.buffer = new char[capacity];
this.head = 0;
this.tail = 0;
this.size = 0;
}
public synchronized void write(String msg) {
for (char c : msg.toCharArray()) {
buffer[head] = c;
head = (head + 1) % capacity;
if (size < capacity) {
size++;
} else {
tail = (tail + 1) % capacity; // 覆盖最旧数据
}
}
}
public synchronized String readAll() {
StringBuilder sb = new StringBuilder(size);
int pos = tail;
for (int i = 0; i < size; i++) {
sb.append(buffer[pos]);
pos = (pos + 1) % capacity;
}
return sb.toString();
}
public synchronized int available() {
return size;
}
public static void main(String[] args) {
RingLogBuffer log = new RingLogBuffer(64);
log.write("INFO: 服务启动完成\n");
log.write("DEBUG: 数据库连接池初始化\n");
log.write("WARN: 内存使用率 85%\n");
log.write("ERROR: 连接超时\n");
System.out.println("=== 日志内容 ===");
System.out.println(log.readAll());
}
}
七、面试要点
| 问题 | 关键要点 |
|---|---|
| 扩容公式 | oldCapacity * 2 + 2,取与需求值较大者 |
| MAX_ARRAY_SIZE 为什么减 8 | JVM 数组头部元数据预留 |
| 为什么不缩容 | 设计上数组只增不减,避免频繁重建 |
| arraycopy 为什么是 native | 底层 memmove 更高效,且能处理内存重叠 |
| reverse 的代理对处理 | UTF-16 代理对占2 char,反转后需要额外修正 |
| append 全类型重载的意义 | 避免自动装箱,int → Integer 有内存和时间开销 |