【Java】 StringBuilder 由浅入深
同样是字符串操作,为什么别人用 StringBuilder 比你快 100 倍?
引言
-介绍可变字符串的概念及其重要性 (看到这个我的第一反映是C++中的<vector>很像)
在编程领域,可变字符串是指内容可动态修改、长度可灵活调整的字符序列,其核心价值在于规避不可变字符串频繁创建新对象的性能损耗 —— 这一点与 C++ 中vector动态调整数组容量的设计思路高度相似,都是通过直接操作底层容器(字符数组 / 动态数组)实现高效的内容修改,而非反复新建实例。
- 对比 String 和 StringBuilder 的差异
Java 中String作为不可变字符串,每一次拼接、修改都会生成新的String对象,底层字符数组无法复用,不仅会产生大量临时对象占用内存,还会触发频繁的垃圾回收;而StringBuilder作为可变字符串的核心实现,所有修改操作均直接作用于内部的字符数组,无需创建新对象,从根源上解决了String的内存与性能问题。
- 适用场景:频繁字符串拼接、修改 (主要还是因为String对内存的占用)
StringBuilder成为频繁字符串拼接、修改场景(如循环组装日志、动态拼接 SQL、批量处理文本)的最优选择,既能大幅降低内存占用,又能显著提升字符串操作的执行效率。
StringBuilder 核心特性
- 可变性:动态调整内部字符数组
底层依托可动态调整长度的字符数组(char [])实现,拼接、插入、删除等修改操作均直接在原数组上完成,无需像 String 那样创建新对象,可灵活调整字符串内容与长度
线程安全性:非线程安全(与 StringBuffer 对比)
属于非线程安全的字符串操作类,其核心方法未加同步锁,多线程并发修改同一实例时易出现数据错乱;而 StringBuffer 为保证线程安全,给所有核心方法添加了 synchronized 锁,代价是性能有所损耗。
性能优势:避免频繁创建新对象
因可直接修改底层数组,避免了 String 不可变导致的频繁创建临时对象和垃圾回收开销,在循环拼接、动态组装字符串等场景下,性能远优于 String,是单线程字符串修改场景的最优选择。
主要方法及使用示例
- append():追加字符串、基本类型、对象
public class Demo {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder();
System.out.println(sb);
System.out.println("--------");
sb.append("abc");
System.out.println(sb);
sb.append("123");
System.out.println(sb);
}
}

- insert():在指定位置插入内容
public class Demo {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder();
System.out.println(sb);
System.out.println("--------");
sb.append("abc");
System.out.println(sb);
sb.insert(0,"插入"); //sb.insert(索引,插入的值)
System.out.println(sb);
}
}

- delete() 和 deleteCharAt():移除字符
public class Demo {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder();
System.out.println(sb);
System.out.println("--------");
sb.append("abcdefg");
System.out.println(sb);
sb.delete(3,5);//索引值包左不包右,删除3-4 de 返回abcfg(删除一串字符)
System.out.println(sb);
sb.deleteCharAt(1);//删除索引为1 ,返回acfg(删除一个字符)
System.out.println(sb);
}
}

- reverse():反转字符串
public class Demo {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder();
System.out.println(sb);
System.out.println("-----");
sb.append("abc");
System.out.println(sb);
sb.reverse();
System.out.println(sb);
}
}

- toString():转换为不可变 String
public class Demo {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder();
System.out.println(sb);
sb.append(123);
String str=sb.toString();//转化为字符串类型
System.out.println(str);
}
}

- setLength():调整字符串长度
public class Demo {
public static void main(String[] args) {
//强制设置字符序列长度,多的直接截断,少的补\0(只填充长度,无内容)
StringBuilder sb = new StringBuilder("abcdefgh");
System.out.println("初始:" + sb + " | 长度:" + sb.length()); // 初始:abcdefgh | 长度:8
// 示例1:新长度 < 当前长度 → 截断
sb.setLength(5); // 保留前5个字符,删除索引5及以后的字符
System.out.println("截断后:" + sb + " | 长度:" + sb.length()); // 截断后:abcde | 长度:5
// 示例2:新长度 > 当前长度 → 补空字符(\0)
sb.setLength(8); // 补充3个空字符,长度恢复为8
System.out.println("补空后:" + sb + " | 长度:" + sb.length()); // 补空后:abcde | 长度:8
// 验证空字符:通过charAt获取索引5的字符(显示为□或空白,ASCII码为0)
System.out.println("索引5的字符ASCII:" + (int) sb.charAt(5)); // 输出:0
// 示例3:设置长度为0 → 清空字符串(最高效的清空方式)
sb.setLength(0);
System.out.println("清空后:" + sb + " | 长度:" + sb.length()); // 清空后: | 长度:0
// 示例4:空字符串设置新长度 → 全是空字符
sb.setLength(3);
System.out.println("空字符串补空后长度:" + sb.length()); // 输出:3
System.out.println("索引1的字符ASCII:" + (int) sb.charAt(1)); // 输出:0
}
}

性能优化技巧
- 初始化时指定容量以减少扩容开销
StringBuilder 底层依赖字符数组存储数据,默认初始容量仅为 16。当追加的字符超过当前容量时,会触发「扩容机制」—— 创建一个新的、容量为原数组 2 倍 + 2 的字符数组,再把原数据拷贝过去。这个过程看似简单,却会产生额外的数组创建和数据拷贝开销,高频操作下(比如循环拼接),多次扩容会让性能断崖式下跌。
// 初始容量16,拼接内容超过16时触发扩容
StringBuilder sb = new StringBuilder();
sb.append("用户ID:10086 | 用户名:张三 | 手机号:13800138000");
// 预估最终字符串长度约50,直接指定容量
StringBuilder sb = new StringBuilder(50);
sb.append("用户ID:10086 | 用户名:张三 | 手机号:13800138000");
链式调用优化代码可读性(链式编程)
新手使用 StringBuilder 时,习惯逐行调用 append()/insert() 等方法,代码充斥大量重复的对象名,既冗余又难读,维护时需要逐行梳理拼接逻辑。
可以根据Sb返回特性直接对返回值进行调用,采用「链式调用」,将多步操作浓缩为一行,逻辑一目了然。
//反例(冗余写法,可读性差)
StringBuilder sb = new StringBuilder(100);
sb.append("订单编号:");
sb.append(20250815001);
sb.append(" | 状态:");
sb.append("已支付");
sb.append(" | 金额:");
sb.append(99.9);
//正确
StringBuilder sb = new StringBuilder(100)
.append("订单编号:").append(20250815001)
.append(" | 状态:").append("已支付")
.append(" | 金额:").append(99.9);
避免在循环中误用 String 拼接
这是最容易踩的坑!循环中用 + 拼接 String,底层会每次循环都创建一个新的String 对象,循环次数越多,临时对象越多,GC 压力越大,性能呈指数级下降。
//错误
String result = "";
// 循环10000次,创建约10000个StringBuilder + 10000个String对象
for (int i = 0; i < 10000; i++) {
result += "数据" + i + ","; // 每次+都触发新对象创建
}
//正确
// 提前指定容量,避免循环内扩容
StringBuilder sb = new StringBuilder(100000);
for (int i = 0; i < 10000; i++) {
sb.append("数据").append(i).append(","); // 仅操作1个对象
}
String result = sb.toString(); // 循环结束后一次性转String
内部实现原理
- 字符数组(char[])结构
- StringBuilder 的底层是一个可动态调整长度的 char 类型数组(源码中字段名为 value),所有字符串操作(append/insert/delete)本质都是对这个数组的直接修改:
- String 的 char [] 数组被 final 修饰,不可修改,每次操作都要新建数组;
- StringBuilder 的 char [] 数组无 final 限制,修改直接在原数组上进行,这是它 “可变” 的核心原因。
自动扩容机制与容量计算
- 当调用 append()/insert() 等方法时,若「需要的最小容量」> 当前数组长度(即 value.length),触发扩容。
- 需要的最小容量 = 现有字符数(count) + 新增字符数;
- 比如:默认容量 16,已存 15 个字符,新增 3 个字符(需要 18),18>16 → 触发扩容。
与 StringBuffer 的底层实现对比
仅在于线程安全有区别

常见问题与解决方案
-多线程环境下的替代方案(StringBuffer)
多线程同时操作同一个 StringBuilder 对象(比如多线程拼接日志、共享业务字符串),会出现字符错乱、数据丢失甚至数组越界异常:
// 多线程操作StringBuilder(错误示例)
public static void main(String[] args) throws InterruptedException {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
sb.append("a"); // 多线程同时append,数据错乱
}
}).start();
}
Thread.sleep(1000);
System.out.println("最终长度:" + sb.length()); // 预期5000,实际远小于5000且数据错乱
}
原因:StringBuilder 无任何线程安全保障:count 字段的修改、char [] 数组的写入都是非原子操作,多线程并发时会出现 “覆盖写入”“计数错误”。
// 兼容多线程:用StringBuffer替代
public static void main(String[] args) throws InterruptedException {
StringBuffer sb = new StringBuffer(); // 线程安全
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
sb.append("a");
}
}).start();
}
Thread.sleep(1000);
System.out.println("最终长度:" + sb.length()); // 准确输出5000
}
内存溢出场景分析与规避
现象:一次性拼接百万 / 千万级字符(比如读取大文件、拼接海量数据),StringBuilder 的 char [] 数组占用内存超过堆空间,触发 OutOfMemoryError: Java heap space。
原因:StringBuilder 的 char [] 数组存储在堆中,超大数组直接耗尽堆内存;若频繁扩容,多次拷贝的临时数组也会加剧内存消耗。
解决
// 分批次拼接大文件内容(避免OOM)
public static void readLargeFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line;
// 每次只缓存1000行,处理完立即输出/写入,释放内存
StringBuilder sb = new StringBuilder(1024 * 100); // 初始容量100KB
int count = 0;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
count++;
if (count >= 1000) {
System.out.print(sb); // 输出/写入文件
sb.setLength(0); // 清空复用,不新建对象
count = 0;
}
}
// 处理剩余内容
if (sb.length() > 0) {
System.out.print(sb);
}
}
}
误用导致的性能问题案例
循环中频繁创建 StringBuilder
// 错误:循环内新建StringBuilder(10000次循环创建10000个对象)
String result = "";
for (int i = 0; i < 10000; i++) {
StringBuilder sb = new StringBuilder(); // 每次循环新建
sb.append("数据").append(i);
result += sb.toString(); // 还叠加了String拼接的坑
}
// 优化:单个StringBuilder复用,零额外对象创建
StringBuilder sb = new StringBuilder(100000); // 提前指定容量
for (int i = 0; i < 10000; i++) {
sb.append("数据").append(i);
}
String result = sb.toString();

1928

被折叠的 条评论
为什么被折叠?



