一、核心概念

  1. 为什么使用 StringBuilder
    • 不可变性的代价String 每次拼接会创建新对象,产生内存碎片和 GC 压力。
    • 可变字符序列StringBuilder 直接修改内部字符数组(char[]),避免中间对象创建,性能提升 10~100 倍
    • 线程安全选择
      • StringBuilder:单线程首选(非线程安全,无同步开销)。
      • StringBuffer:多线程环境使用(方法用 synchronized 修饰)。

二、操作步骤(以日志处理为例)

场景:拼接用户行为日志(UserID + Action + Timestamp)

// 1. 创建 StringBuilder 并预分配容量(减少扩容次数)
StringBuilder logBuilder = new StringBuilder(200); // 预估日志长度

// 2. 链式追加数据(避免多次调用)
logBuilder.append("UserID: ").append(userId)
          .append(", Action: ").append(action)
          .append(", Timestamp: ").append(System.currentTimeMillis());

// 3. 转换为最终字符串(仅在需要时调用 toString())
String logEntry = logBuilder.toString();

// 4. 重置复用(避免重复创建对象)
logBuilder.setLength(0); // 清空内容,保留底层数组

动态 SQL 生成(防 SQL 注入版)

List<String> conditions = Arrays.asList("age > 25", "status = 'ACTIVE'");
StringBuilder sqlBuilder = new StringBuilder("SELECT * FROM users WHERE ");
boolean isFirst = true;

for (String cond : conditions) {
    if (!isFirst) {
        sqlBuilder.append(" AND ");
    }
    sqlBuilder.append(cond); // 条件本身需预校验,避免拼接未过滤的用户输入
    isFirst = false;
}

// 使用预编译防止注入(非字符串拼接)
String sql = sqlBuilder.toString();
PreparedStatement stmt = connection.prepareStatement(sql);

三、常见错误与注意事项

  1. 循环内错误使用 + 拼接

    • 错误示例
      String result = "";
      for (int i = 0; i < 10000; i++) {
          result += i; // 每次循环隐式创建 StringBuilder 对象
      }
      
    • 后果:性能下降数十倍,内存激增。
  2. 忽略初始容量导致频繁扩容

    • 默认容量 16 字符,扩容需复制数组。
    • 优化:预估最终长度,通过 new StringBuilder(initialCapacity) 指定。
  3. 多线程共享 StringBuilder

    • 非线程安全的 StringBuilder 在并发修改时会导致数据错乱。
    • 解决:改用 StringBuffer 或通过 ThreadLocal 隔离。
  4. 频繁中间修改引发内存碎片

    • StringBuilder 中间频繁插入/删除会使内存不连续,遍历性能下降超 1000 倍
    • 解决:每修改 250 次后重建 StringBuilder 或改用专用数据结构(如 PieceTable)。

四、使用技巧与性能优化

  1. 预分配容量

    int estimatedLength = 5000;
    StringBuilder sb = new StringBuilder(estimatedLength + 100); // 增加安全余量
    
    • 效果:减少扩容次数,性能提升 50% 以上。
  2. 链式调用 vs 分步调用

    • 优先链式调用:减少方法调用栈开销。
      // ✅ 推荐
      sb.append("a").append("b");
      // ❌ 避免
      sb.append("a");
      sb.append("b");
      
  3. 复用对象降低 GC 压力

    • 清空内容而非新建对象:
      sb.setLength(0); // 复用底层数组
      
    • 结合对象池(如 Apache Commons Pool)管理实例。
  4. 异步日志记录

    • 将日志拼接与写入分离,避免阻塞主线程:
      ExecutorService executor = Executors.newSingleThreadExecutor();
      executor.submit(() -> logger.info(logBuilder.toString()));
      

五、高频场景最佳实践

1. 日志处理

  • 优化点
    • 使用模板引擎(如 String.format())替代手动拼接。
    • 批量追加日志行后一次性写入(减少 I/O 次数)。
  • 示例
    StringBuilder batchLog = new StringBuilder(4096);
    for (LogEvent event : events) {
        batchLog.append(event.format()).append("\n");
    }
    fileWriter.write(batchLog.toString());
    

2. 动态 SQL 生成

  • 安全与性能兼顾
    • 禁止直接拼接用户输入:使用预编译(PreparedStatement)防注入。
    • 利用 ORM 框架:如 MyBatis 的 <if> 标签,保持 SQL 结构一致性,复用查询计划。
      <select id="findUsers">
        SELECT * FROM users
        <where>
          <if test="name != null">AND name = #{name}</if>
        </where>
      </select>
      
  • 索引优化:确保 WHERE 条件字段有索引,避免全表扫描。

六、性能优化总结

策略 效果 适用场景
预分配容量 减少扩容复制,提升 30%~50% 性能 已知最终长度的大文本拼接
避免循环内 + 拼接 性能提升 10~100 倍 所有循环拼接场景
复用 StringBuilder 降低 GC 频率,减少对象创建开销 高频调用(如日志每请求多次)
异步处理 避免主线程阻塞,提升吞吐量 日志写入、非关键路径操作
使用预编译 SQL 防止注入,复用查询计划 动态 SQL 生成

💡 黄金法则:单线程用 StringBuilder,多线程用 StringBuffer;循环拼接必用 StringBuilder;预分配容量是性价比最高的优化手段。