← 返回题目列表

String、StringBuilder、StringBuffer 有什么区别?

高频 简单 第 4 / 32 题 更新于 2026/07/26
StringStringBuilderStringBuffer字符串

简化版

三者区别在可变性线程安全String 不可变(每次「修改」都新建对象),适合少量、不常变的字符串;StringBuilder 可变、非线程安全,在同一个内部字符数组上追加,性能最好,是单线程拼接字符串的首选;StringBuffer 可变、线程安全(方法加 synchronized),但因为加锁性能比 StringBuilder 略低。口诀:不变用 String,单线程拼接用 StringBuilder,多线程共享拼接用 StringBuffer。

详细版

维度StringStringBuilderStringBuffer
可变性不可变(final char[]/byte[])可变可变
线程安全安全(因不可变)不安全安全(方法 synchronized)
性能频繁修改差(每次新建对象)最快比 Builder 慢(有锁开销)
引入版本JDK 1.0JDK 5JDK 1.0
适用少量、不变的字符串单线程大量拼接多线程共享的拼接

为什么 String 频繁修改性能差:String 不可变,每次拼接都创建新对象:

String s = "";
for (int i = 0; i < 10000; i++) {
    s += i;   // 每次都 new 一个 StringBuilder 拼接再 toString,产生大量临时对象
}
// 循环内反复创建/丢弃对象,O(n²) 级开销

// 正确:复用同一个 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);   // 在同一个数组上追加,满了才扩容
}
String result = sb.toString();

StringBuilder 和 StringBuffer 的关系:两者都继承自 AbstractStringBuilder,方法逻辑几乎一样,唯一区别是 StringBuffer 的方法都加了 synchronized。所以 StringBuilder 是 StringBuffer 的「去锁高速版」。

⚠️ 编译器会对常量字符串拼接做优化:String s = "a" + "b" + "c" 在编译期就直接合并成 "abc",不产生中间对象。真正低效的是循环里对变量的 += 拼接——那才会反复创建 StringBuilder。

完整版教学

一、可变性是三者一切差异的根源

抓住一个词就能推出全部区别:可变性

String     不可变 → 每次"改"都新建对象 → 频繁改则大量垃圾对象 → 慢,但天生线程安全
StringBuilder/StringBuffer 可变 → 在同一个内部 char[]/byte[] 上原地追加 → 快
    其中 StringBuffer 给方法加了锁 → 线程安全但略慢
    StringBuilder 不加锁 → 单线程最快

String 不可变的实现是内部字符数组用 final 修饰、且类本身 final(不能被继承破坏)。而 StringBuilder/StringBuffer 内部的数组不是 final,可以扩容、可以原地改,这就是「可变」。理解「不可变 → 新建对象 → 慢但安全」「可变 → 原地改 → 快」这条链,三者的性能和线程安全差异就都通了。

二、String 的「修改」为什么是新建对象

String 的所有「修改」方法(concat、replace、substring、+)都返回新 String,原对象不变

String a = "hello";
String b = a.concat(" world");   // a 还是 "hello",b 是新对象 "hello world"
a.replace('l', 'L');             // 返回新串 "heLLo",但没接收,a 依旧是 "hello"

+= 拼接更隐蔽——它被编译器翻译成「new StringBuilder().append().toString()」:

s += i  在循环里等价于:
  s = new StringBuilder(s).append(i).toString();
每次循环:new 一个 StringBuilder + 把已有 s 全部拷进去 + append + toString 生成新 String
n 次循环,第 k 次要拷贝长度约 k 的字符串 → 总拷贝量 1+2+...+n = O(n²)

10000 次循环的字符串 +=,实际做了约 5000 万次字符拷贝,还产生上万个临时对象。这就是「String 频繁拼接慢」的真相——不是慢一点,是复杂度从 O(n) 退化成 O(n²)。

三、StringBuilder 为什么快:原地追加 + 扩容

StringBuilder 内部维护一个 char[](Java 9+ 是 byte[]),append 时直接往数组后面写,满了才扩容(新容量约 2 倍 + 2):

StringBuilder sb(初始容量16):
append("abc") → [a,b,c,_,_,...]        直接写,不新建对象
append 到满  → 扩容成约 2 倍,Arrays.copyOf 一次拷贝
n 次 append:每次 O(1)(均摊),偶尔扩容,总复杂度 O(n)

对比 String 的 O(n²),StringBuilder 是 O(n)。关键差异是 StringBuilder 全程复用同一个数组、只在扩容时拷贝一次,而 String 每次都全量拷贝。如果预估最终长度,new StringBuilder(estimatedSize) 预设容量能连扩容都省掉,进一步提速。

四、StringBuffer 的线程安全:方法级 synchronized

StringBuffer 和 StringBuilder 共享父类 AbstractStringBuilder,逻辑相同,区别只在 StringBuffer 给每个方法加了 synchronized

// StringBuffer
public synchronized StringBuffer append(String str) { ... }
// StringBuilder
public StringBuilder append(String str) { ... }   // 无锁

所以多线程同时 append 同一个 StringBuffer 不会数据错乱。但要注意两点:① 单线程用 StringBuffer 是白交锁开销(没有竞争还要走锁流程);② 方法级锁只保证单次 append 原子,多次 append 的组合仍不原子——如果业务要求「连续几次 append 之间不被打断」,还得在外层自己加锁。实际上多线程共享一个可变字符串拼接的场景很少,所以 StringBuffer 用得远不如 StringBuilder 多。

五、性能实测数量级对比

三者拼接 5 万次的相对耗时,能直观感受差距:

方式相对耗时原因
String +=极慢(秒级)O(n²),上万临时对象 + GC 压力
StringBuffer快(毫秒级)O(n),但每次 append 加解锁
StringBuilder最快(毫秒级,略快于 Buffer)O(n),无锁

数量级差异(String 比 Builder 慢成百上千倍)主要来自算法复杂度,锁开销(Buffer vs Builder)只是小头。所以选型第一优先级是「别在循环里用 String 拼接」,其次才是「单线程用 Builder 而非 Buffer」。

六、编译器优化:哪些拼接其实不慢

不是所有 + 都慢,编译器对编译期常量会直接合并:

String s1 = "a" + "b" + "c";        // 编译期直接变成 "abc",零运行时开销
final String x = "a";
String s2 = x + "b";                // x 是编译期常量,也会优化

String y = getStr();                // 运行时才知道值
String s3 = y + "b";                // 这个才会 new StringBuilder(单次,无所谓)

for (...) { s += i; }               // ★ 循环里的变量拼接才是真的慢(反复 new StringBuilder)

判断标准:单次的、编译期能定的拼接不用管;只有「循环/大量」的变量拼接才需要换 StringBuilder。很多人一看到 + 就换 StringBuilder,其实单次拼接用 + 更可读,编译器已经优化过了。

记忆钩子:「String 不可变(改=新建,O(n²))、StringBuilder 可变无锁最快、StringBuffer 可变加锁线程安全;循环拼接必用 Builder,单次 + 交给编译器」

七、常见误区与追问

  • 误区:所有字符串拼接都要用 StringBuilder。 单次或编译期常量拼接用 + 更可读且已被编译器优化;只有循环/大量变量拼接才需要 StringBuilder。
  • 误区:String 拼接只是慢一点。 循环里 += 是 O(n²),会产生大量临时对象,比 StringBuilder 慢成百上千倍,是数量级差异。
  • 误区:StringBuffer 一定比 StringBuilder 慢很多。 慢的主要是锁开销,量级不大;两者都是 O(n),与 String 的 O(n²) 才是本质差距。
  • 误区:StringBuffer 线程安全,所以多次 append 也原子。 只保证单次 append 原子,多次 append 的组合仍需外层加锁才能整体原子。
  • 追问:StringBuilder 和 StringBuffer 底层一样吗? 是的,都继承 AbstractStringBuilder,逻辑相同,唯一区别是 StringBuffer 方法加了 synchronized。
  • 追问:String 的 + 底层怎么实现? 编译期常量直接合并;运行时变量拼接被翻译成 new StringBuilder().append().toString(),单次无所谓,循环里则反复创建导致慢。
  • 追问:为什么 String 设计成不可变? 便于常量池共享、可缓存 hashCode、天生线程安全、可安全作为 HashMap 的 key 和方法参数——不可变是安全和性能的综合取舍。

八、加强记忆

三者的一切差异都从「可变性」推导:String 不可变——每次「修改」都新建对象,循环 += 会退化成 O(n²) 并产生大量临时对象,但天生线程安全、可共享、可缓存 hashCode;StringBuilder 可变、无锁——在同一个内部数组上原地追加、满了才扩容,O(n),单线程拼接最快;StringBuffer 可变、加锁——和 Builder 同源(都继承 AbstractStringBuilder),只是方法加了 synchronized,线程安全但略慢,且只保证单次 append 原子。选型口诀「不变用 String、单线程拼接用 StringBuilder、多线程共享用 StringBuffer」,但第一优先级是「别在循环里用 String 拼接」——那是数量级的性能坑,而单次或编译期常量的 + 编译器已优化、无需操心。一句话「改=新建的是 String(慢但安全)、原地改无锁的是 Builder(最快)、原地改加锁的是 Buffer(安全略慢)」。