Java基础-10:彻底搞懂Java String, StringBuffer, StringBuilder底层原理和避坑指南

0 阅读5分钟

在Java开发中,字符串处理是最常见的操作之一。然而,很多开发者对 StringStringBufferStringBuilder 的区别仅停留在“可变/不可变”或“线程安全/非线程安全”的表面理解上。本文将从底层源码实现出发,深入剖析三者的内部机制,并结合最佳实践常见陷阱,帮助你彻底掌握它们的使用之道。


一、核心区别概览

特性StringStringBufferStringBuilder
可变性不可变(Immutable)可变(Mutable)可变(Mutable)
线程安全是(因不可变)是(synchronized)
底层存储final char[] valuechar[] value(非final)char[] value(非final)
性能拼接性能差(频繁创建新对象)中等(同步开销)最高(无同步)
适用场景常量、少量拼接多线程环境下的字符串构建单线程下的高性能拼接

二、底层源码深度解析

1. String:不可变性的根源

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    private final byte[] value; // Java 9+ 使用 byte[] + coder 字段优化内存
    // Java 8 及之前:private final char[] value;
    
    // 所有修改操作(如 concat, replace)都返回新 String 对象
    public String concat(String str) {
        if (str.isEmpty()) return this;
        return new String(value, true).concat(str); // 实际创建新对象
    }
}

关键点

  • final 类 + final 字段 → 不可变
  • 任何“修改”操作都会创建新对象,旧对象保留在常量池或堆中
  • Java 9 起,为节省内存,String 内部改用 byte[] 存储,并通过 coder 字段标识是 LATIN1 还是 UTF16 编码

📌 不可变性的好处:线程安全、可缓存(字符串常量池)、可用作 HashMap 的 key。


2. StringBuffer:线程安全的可变字符串

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
}
  • 继承自 AbstractStringBuilder
  • 所有 public 方法都加了 synchronized → 线程安全
  • 内部使用 char[] value(非 final),可动态扩容

3. StringBuilder:高性能的单线程选择

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
}
  • 同样继承 AbstractStringBuilder
  • 方法无 synchronized → 非线程安全,但性能更高
  • StringBuffer 共享大部分逻辑(如扩容策略)

4. AbstractStringBuilder:共享的核心逻辑

StringBufferStringBuilder 的核心实现在 AbstractStringBuilder 中:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    int count; // 当前字符数

    public AbstractStringBuilder append(String str) {
        if (str == null) str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len); // 扩容检查
        str.getChars(0, len, value, count);  // 复制字符
        count += len;
        return this;
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value, newCapacity(minimumCapacity));
        }
    }

    private int newCapacity(int minCapacity) {
        int newCapacity = (value.length << 1) + 2; // 扩容为原长度*2+2
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        return newCapacity;
    }
}

扩容策略
默认容量 16,当空间不足时,新容量 = 原容量 * 2 + 2。若仍不够,则直接使用所需最小容量。


三、性能对比实验

// 测试:拼接 10 万次 "a"
long start = System.currentTimeMillis();

// 方式1:String +=
String s = "";
for (int i = 0; i < 100_000; i++) s += "a"; // 极慢!O(n²)

// 方式2:StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) sb.append("a"); // 快!

// 方式3:StringBuffer
StringBuffer buf = new StringBuffer();
for (int i = 0; i < 100_000; i++) buf.append("a"); // 比 StringBuilder 慢约 10~30%

System.out.println(System.currentTimeMillis() - start);

结果(JDK 17,典型值):

  • String +=:> 10,000 ms(不推荐)
  • StringBuilder:≈ 5 ms
  • StringBuffer:≈ 7 ms

💡 注意:现代编译器(如 JDK 8+)会对局部变量+= 操作自动优化为 StringBuilder,但跨方法或循环内多次拼接仍会退化


四、最佳实践与避坑指南

✅ 正确使用姿势

  1. 字符串常量/少量拼接 → 用 String

    String msg = "Hello, " + name + "!"; // 编译期优化为 StringBuilder
    
  2. 单线程大量拼接 → 用 StringBuilder

    StringBuilder sb = new StringBuilder(1024); // 预估容量避免多次扩容
    for (String item : list) sb.append(item).append("\n");
    return sb.toString();
    
  3. 多线程共享构建 → 用 StringBuffer(但更推荐同步外部控制)

    // 更佳方案:用 StringBuilder + 外部 synchronized
    private final StringBuilder sharedBuilder = new StringBuilder();
    
    public synchronized void append(String s) {
        sharedBuilder.append(s);
    }
    

⚠️ 常见陷阱与避坑

坑1:误以为 String += 总是高效

// 错误:在循环中使用 +=
String result = "";
for (int i = 0; i < n; i++) {
    result += items[i]; // 每次都 new StringBuilder + toString()
}

修复:改用 StringBuilder

坑2:忽略初始容量导致频繁扩容

// 默认容量16,若拼接1000字符,会扩容多次
StringBuilder sb = new StringBuilder(); 

修复:预估大小

StringBuilder sb = new StringBuilder(expectedSize);

坑3:在多线程中误用 StringBuilder

// 多线程并发 append 可能导致数组越界或数据错乱!
static StringBuilder sb = new StringBuilder();

修复:改用 StringBuffer 或线程局部变量(ThreadLocal<StringBuilder>

坑4:混淆 equals() 行为

StringBuffer sb1 = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer("hello");
System.out.println(sb1.equals(sb2)); // false!因为未重写 equals()

注意StringBuffer/StringBuilderequals() 是引用比较,不要用于内容比较。应转为 String 后再比较:

sb1.toString().equals(sb2.toString());

五、总结

场景推荐类型
字符串常量、配置项、keyString
单线程内大量拼接(日志、JSON 构建等)StringBuilder(带初始容量)
多线程共享且必须在线程内拼接StringBuffer(但优先考虑外部同步)
需要内容比较统一转为 String 后使用 equals()

核心原则

  • 不可变用 String,可变拼接用 StringBuilder,线程安全需求才考虑 StringBuffer
  • 永远不要在循环中用 String += 拼接
  • 预估容量,减少扩容开销
  • 多线程下慎用 StringBuilder

掌握这些底层原理与实践技巧,你就能在字符串处理上写出高性能、无 bug 的 Java 代码!


📚 延伸阅读

  • 《Java Performance: The Definitive Guide》
  • OpenJDK 源码:java.lang.String, java.lang.AbstractStringBuilder
  • JEP 254: Compact Strings(Java 9 字符串内存优化)

作者:架构师Beata
日期:2026年2月9日
声明:本文基于网络文档整理,如有疏漏,欢迎指正。转载请注明出处。
互动:如有任何问题?欢迎在评论区分享!