【Java】字符串拼接优化:算法竞赛中 StringBuilder 的绝杀技巧

【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();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值