源本科技 | 码上会

String vs StringBuilder vs StringBuffer

2025/12/26
103
0

学习目标

  • 理解三者在可变性、线程安全性和性能上的本质区别

  • 掌握在不同场景下如何正确选择合适的字符串类

  • 能通过代码验证三者的实际行为差异

  • 避免因误用导致的性能问题或并发错误


核心特性对比

特性

String

StringBuilder

StringBuffer

可变性

❌ 不可变(Immutable)

✅ 可变(Mutable)

✅ 可变(Mutable)

线程安全

✅(因不可变天然安全)

❌ 非线程安全

✅ 线程安全(方法同步)

性能

慢(频繁创建新对象)

最快(无同步开销)

中等(同步锁开销)

JDK 引入版本

JDK 1.0

JDK 5

JDK 1.0

底层存储

final char[] value

char[] value(非 final)

char[] value(非 final)

关键理解

  • String 的“安全”源于不可变性,而非同步机制

  • StringBuilderStringBuffer 共享几乎相同的 API,唯一区别是线程安全性


行为验证

String 不可变性

public class CoderHub {
    public static void main(String[] args) {
        String str = "Hello";
        str.concat(" World"); // 返回新对象,但未赋值
        System.out.println(str); // 输出: Hello
        
        // 正确使用方式(但效率低)
        str = str.concat(" World");
        System.out.println(str); // Hello World
    }
}

📌 concat()+substring() 等操作均返回新 String 对象,原对象不变。


StringBuilder 高效修改

StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 直接修改内部 char[]
System.out.println(sb); // Hello World

✅ 所有操作原地完成,无新对象产生。


StringBuffer 线程安全

StringBuffer sb = new StringBuffer("Hello");
sb.append(" World"); // 方法声明为 synchronized
System.out.println(sb); // Hello World

🔒 每个公共方法都加了 synchronized 锁,确保多线程下操作原子性。


使用场景

何时使用 String

  • 字符串内容不会改变(如配置常量、字面量)

  • 作为键(Key) 存入 HashMap(依赖不可变性保证哈希一致性)

  • 简单的、少量的字符串拼接(编译器会优化为 StringBuilder

// 推荐
final String APP_NAME = "CoderHub";
String message = "User " + userId + " logged in."; // 编译期优化

何时使用 StringBuilder

  • 单线程环境下的高频字符串操作(如日志拼接、JSON 构建)

  • 循环中动态构建字符串

  • 性能敏感的文本处理

// 推荐:循环拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("Item ").append(i).append("\n");
}
String result = sb.toString();

何时使用 StringBuffer

  • 多线程共享同一个可变字符串对象

  • 需要线程安全且无法使用其他同步机制的场景

// 多线程日志收集器(简化示例)
public class ThreadSafeLogger {
    private final StringBuffer log = new StringBuffer();
    
    public void addLog(String msg) {
        log.append(Thread.currentThread().getName())
           .append(": ").append(msg).append("\n");
    }
    
    public String getLog() {
        return log.toString();
    }
}

注意:现代开发中,更推荐使用 java.util.concurrent 工具或局部 StringBuilder + 合并结果,而非直接共享 StringBuffer


性能对比实验

假设执行 10,000 次字符串追加:

方式

对象创建次数

内存分配

执行时间(相对)

String +=

~10,000

100x(最慢)

StringBuilder

1(初始)+ 扩容次数

1x(最快)

StringBuffer

1(初始)+ 扩容次数

~2–5x(同步开销)

实际测试中,StringBuilder 在单线程下通常比 StringBuffer20%~50%


常见误区

❌ 误区 1:“StringBuffer 比 StringBuilder 功能更强”

事实:两者 API 完全一致,唯一区别是同步

❌ 误区 2:“String 拼接总是低效”

事实:编译器会对编译期确定的拼接优化为 StringBuilder

String s = "A" + "B" + "C"; // 编译为 "ABC"(常量折叠)
String s = "Hello " + name;  // 编译为 new StringBuilder(...).toString()

❌ 误区 3:“多线程必须用 StringBuffer”

事实:若每个线程使用独立的 StringBuilder,再合并结果,性能更高且线程安全。


重点总结

  • String:内容固定 → 安全、简洁、适合常量

  • StringBuilder:单线程高频修改 → 首选,性能最优

  • StringBuffer:多线程共享可变字符串 → 线程安全,但有性能代价

黄金法则默认用 String,拼接用 StringBuilder,仅在明确需要线程安全时才用 StringBuffer


思考题

  1. 为什么 Java 设计 String 为不可变类?这种设计对 JVM 内存管理(如字符串常量池)有何影响?

  2. 编写一个多线程程序,分别使用 StringBuilderStringBuffer 进行共享字符串追加,观察 StringBuilder 可能出现的数据错乱现象。

  3. 在 Android 或 Spring Boot 项目中,你更可能在哪里看到 StringBuilder 的使用?为什么这些场景不适合用 String