Java StringBuffer 和 StringBuilder 类(一文讲透)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

引言:字符串的“不可变”困境与解决方案

在 Java 编程中,字符串(String)的不可变性(Immutable)是一个核心特性。它确保了字符串对象创建后内容无法被修改,这虽然带来了安全性与线程安全的优势,但也带来了性能问题。例如,当需要频繁修改字符串内容时(如循环拼接字符串),每次操作都会生成新对象,导致内存浪费和效率低下。

为解决这一问题,Java 提供了 StringBufferStringBuilder 两个类。它们允许直接修改字符串内容,从而在特定场景下大幅提升性能。本文将深入讲解这两个类的原理、区别、使用场景及最佳实践,帮助开发者在实际开发中合理选择与应用。


一、不可变性的代价:为什么需要可变字符串类?

1. 字符串的“拼图”困境

字符串的不可变性如同一块完整的拼图:每次修改(如添加或删除字符)都会生成一块全新的拼图,而非修改原图。例如:

String s = "Hello";  
s += " World"; // 实际生成了一个新对象 "Hello World"  

这种机制在少量操作时无感,但当循环中频繁拼接字符串时,性能会显著下降。例如:

String result = "";  
for (int i = 0; i < 10000; i++) {  
    result += i; // 每次循环生成新对象  
}  

上述代码会创建约 10000 个临时对象,内存消耗极大。

2. 可变字符串类的“积木”特性

StringBuffer 和 StringBuilder 类似“积木块”,允许直接修改内部存储的字符序列。它们通过动态扩容的字符数组(char[])保存内容,修改操作(如追加、插入)直接作用于原数组,避免了频繁创建新对象的开销。


二、StringBuffer 和 StringBuilder 的核心区别

1. 核心功能与实现

两者的主要区别在于 线程安全

  • StringBuffer:所有方法均通过 synchronized 关键字修饰,保证线程安全,但同步机制会带来性能损耗。
  • StringBuilder:无线程安全机制,性能更高,适用于单线程环境。

2. 性能对比(通过表格直观展示)

特性StringBufferStringBuilder
线程安全是(同步方法)
适用场景多线程环境单线程或无需线程安全场景
性能较低(因同步开销)更高
继承关系继承自 Object继承自 Object
继承抽象类/接口实现 Appendable, Serializable实现 Appendable

3. 选择建议

  • 单线程场景:优先使用 StringBuilder(性能更高)。
  • 多线程场景:若需共享可变字符串,使用 StringBuffer;或改用线程安全的替代方案(如 ConcurrentHashMap 中的原子操作)。

三、内部实现机制:动态扩容与字符数组

1. 字符数组与容量管理

两者均通过 char[] 数组存储内容,初始容量默认为 16。当容量不足时,会动态扩容:

  • 扩容策略:新容量 = 原容量 × 2(或根据实际需求调整)。

例如,初始容量为 16 的数组追加第 17 个字符时,会扩容为 32:

StringBuilder sb = new StringBuilder("Hello"); // 初始容量 16  
sb.append(" World"); // 容量足够,直接追加  
sb.append(" 123456789"); // 可能触发扩容  

2. 核心方法解析

  • append():追加字符、字符串、数字等类型。
  • insert():在指定位置插入内容。
  • delete():删除指定范围的字符。
  • reverse():反转字符串。

示例代码

StringBuilder sb = new StringBuilder("Java");  
sb.append(" is "); // 追加字符串  
sb.append(2023);   // 追加整数  
sb.insert(0, "Great "); // 在开头插入 "Great "  
System.out.println(sb); // 输出 "Great Java is 2023"  

四、线程安全与性能的权衡

1. 线程安全的代价

StringBuffer 的同步机制通过 synchronized 关键字实现,确保同一时间只有一个线程能执行修改操作。然而,这种机制会显著降低性能。

性能测试案例

public static void main(String[] args) {  
    long start = System.currentTimeMillis();  
    StringBuffer sb = new StringBuffer();  
    for (int i = 0; i < 1000000; i++) {  
        sb.append("a");  
    }  
    System.out.println("StringBuffer: " + (System.currentTimeMillis() - start) + " ms");  

    start = System.currentTimeMillis();  
    StringBuilder sb2 = new StringBuilder();  
    for (int i = 0; i < 1000000; i++) {  
        sb2.append("a");  
    }  
    System.out.println("StringBuilder: " + (System.currentTimeMillis() - start) + " ms");  
}  

测试结果(示例):

  • StringBuffer:约 200 ms
  • StringBuilder:约 10 ms

2. 多线程场景的误区

即使在多线程环境中,也未必需要使用 StringBuffer。例如:

  • 若多个线程仅读取字符串内容(无需修改),则无需同步。
  • 若多个线程需要修改同一对象,建议改用线程本地变量(ThreadLocal)或外部同步机制。

五、使用场景与最佳实践

1. 场景分析

场景推荐类原因
单线程频繁修改字符串StringBuilder性能最优
多线程共享可变字符串StringBuffer 或其他线程安全方案避免数据竞争
需要与遗留代码兼容StringBuffer早期 Java 版本仅支持 StringBuffer

2. 常见误区与解决方法

  • 误区 1:认为 StringBuffer 总是更安全。
    • 解决:在单线程中使用 StringBuilder,多线程时需评估是否需要共享可变对象。
  • 误区 2:过度依赖 append() 的便捷性。
    • 解决:预分配足够容量(如 new StringBuilder(初始容量)),减少扩容次数。

3. 最佳实践

  • 优先选择 StringBuilder:除非明确需要线程安全。
  • 避免在多线程中共享 StringBuilder:若必须共享,改用 StringBuffer 或线程安全集合。
  • 合理预分配容量:例如 new StringBuilder(list.size() * 2)

六、总结:选择最适合的可变字符串工具

通过本文的讲解,我们明确了以下关键点:

  1. String 的不可变性导致频繁修改时性能低下,而 StringBuffer 和 StringBuilder 通过可变字符数组解决这一问题。
  2. 线程安全是选择的关键:多线程场景用 StringBuffer,单线程场景用 StringBuilder。
  3. 性能优化需结合场景:预分配容量、避免不必要的同步可显著提升效率。

在实际开发中,开发者需根据具体需求(如线程环境、性能要求)灵活选择工具。例如,日志拼接、数据缓存等场景中,StringBuilder 几乎是默认选择;而多线程共享的计数器或状态维护,则需权衡性能与线程安全。

掌握这两个类的特性与用法,将帮助开发者写出更高效、更健壮的 Java 代码。

最新发布