4 AbstractStringBuilder —— 可变字符串的骨架实现

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 有内存和时间开销
相关推荐
日月云棠12 小时前
2 Object —— Java 类体系的根节点
java·后端
瀚高PG实验室12 小时前
开发管理工具打不开No way to find ori gi nal streamhand er for jar protocol
java·数据库·jar·瀚高数据库
woniu_buhui_fei12 小时前
ArrayList核心逻辑
java·开发语言
亦暖筑序12 小时前
Spring AI Alibaba 1.1.2 实战:5种多Agent编排模式完全指南
java·spring boot·ai编程
happyprince12 小时前
05-Hugging Face Transformers 缓存系统深度分析
java·spring·缓存
大数据三康12 小时前
Java静态常量与静态导入:计算圆面积
java·开发语言
ping某13 小时前
达梦官方库排查 dmPython 安装后 python -m 报错:.pth + os.execve 的排查实录
后端
moMo13 小时前
前后端模块化分离,web盒子布局思维
前端·后端
程序员牛奶13 小时前
[Algo-3]前缀和秒杀两道区间求和题:一维 + 二维统一模板
后端·算法