String、StringBuilder、StringBuffer 的区别?

2025年 阅读约 8 分钟 面试指南 · Java面试

深入解析Java中String、StringBuilder、StringBuffer的区别,从不可变性到线程安全,分初级、中级、高级三个层次全面讲解。

一句话总结

String 不可变(final 类 + private final byte[],适合常量/Map key)→ StringBuilder 可变且快(非线程安全,单线程拼接首选)→ StringBuffer 可变且安全(synchronized 方法,多线程场景)。JDK 9 引入 Compact Strings(byte[] + coder),纯 Latin-1 字符内存减半。字符串常量池通过 intern() 复用相同字面量。

初级理解

String 是不可变的:String 类被 final 修饰,底层使用 private final char[](JDK 9 后改为 byte[])存储字符。每次对 String 的修改操作(如拼接、替换)都会创建一个新的 String 对象,原对象不变。

StringBuilder 和 StringBuffer 是可变的:它们都继承自 AbstractStringBuilder,内部使用可变的 char[] 数组,修改操作在原数组上进行,不会创建新对象。

线程安全性:StringBuffer 的方法都加了 synchronized,是线程安全的;StringBuilder 没有同步,线程不安全但性能更高。

特性StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全安全(不可变天然安全)不安全安全(synchronized)
性能拼接时最差最高中等
使用场景少量字符串操作单线程大量拼接多线程大量拼接
一句话总结:String 不可变适合常量;StringBuilder 可变且快适合单线程;StringBuffer 可变且安全适合多线程。

中级深入

String 的"+"拼接底层原理:Java 编译器会对 String 的"+"操作进行优化。常量拼接在编译期完成,变量拼接则使用 StringBuilder:

// 编译前 String s = "a" + "b" + "c"; // 编译后(常量折叠) String s = "abc"; // 编译前 String s = a + b; // 编译后(等价于) String s = new StringBuilder().append(a).append(b).toString();

循环拼接的性能陷阱:在循环中使用 String 的"+"拼接,每次循环都会创建一个新的 StringBuilder 和 String 对象,导致大量 GC:

// 错误写法:每次循环创建新 StringBuilder String result = ""; for (int i = 0; i < 10000; i++) { result += i; // 等价于 result = new StringBuilder(result).append(i).toString() } // 正确写法:复用同一个 StringBuilder StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(i); } String result = sb.toString();

StringBuilder 的扩容机制:默认初始容量为 16。当 append 时容量不足,会扩容为 原容量 * 2 + 2,并将原数组复制到新数组。如果知道最终长度,建议在构造时指定容量避免频繁扩容。

// 推荐:预估容量,避免扩容 StringBuilder sb = new StringBuilder(1024);

高级拓展

字符串常量池(String Pool):JVM 在堆中维护了一个字符串常量池。使用双引号直接创建的字符串会放入常量池,使用 new String() 创建的不会。可以通过 intern() 方法手动将字符串加入常量池。

String s1 = "hello"; // 放入常量池 String s2 = "hello"; // 从常量池获取,s1 == s2 为 true String s3 = new String("hello"); // 在堆中新建对象,不在常量池 String s4 = s3.intern(); // 将 s3 的值放入常量池并返回引用 System.out.println(s1 == s2); // true System.out.println(s1 == s3); // false System.out.println(s1 == s4); // true

JDK 9 的 Compact Strings 优化:JDK 9 将 String 底层从 char[] 改为 byte[],并增加了一个 coder 字段标识编码方式(LATIN1 或 UTF16)。如果字符串全是 Latin-1 字符(即每个字符占 1 字节),则使用 LATIN1 编码,内存占用减半。

String 为什么设计为不可变?

1. 字符串常量池的需要:如果 String 可变,常量池中的字符串被修改会影响所有引用它的变量。

2. 安全性:String 广泛用于类名、文件路径、网络连接参数等,不可变性保证了这些关键信息不会被篡改。

3. HashMap 的 key:String 作为最常用的 HashMap key,不可变性保证了 hashCode 不会变化,避免哈希表出现不一致。

4. 线程安全:不可变对象天然线程安全,无需同步。

面试加分项:能说出 JDK 9 Compact Strings 优化和 String 不可变性的设计原因,说明你对 JDK 演进有持续关注。

实战场景

场景:日志拼接性能优化

// 反例:每次循环创建 StringBuilder + String,大量 GC for (int i = 0; i < 100000; i++) { log.debug("处理第" + i + "条数据,结果:" + result); } // 正例1:先判断日志级别,避免无效拼接 if (log.isDebugEnabled()) { log.debug("处理第" + i + "条数据,结果:" + result); } // 正例2:使用参数化日志(SLF4J) log.debug("处理第{}条数据,结果:{}", i, result); // 不拼接,直接传参

场景:大量字符串拼接选型

// 少量拼接(<5次):直接用 + String sql = "SELECT * FROM user WHERE id = " + userId; // 中等拼接(5-100次):StringBuilder StringBuilder sb = new StringBuilder(); for (String item : list) { sb.append(item).append(","); } // 大量拼接(>100次)或需要分隔符:StringJoiner / Stream String result = list.stream().collect(Collectors.joining(",")); // 多线程拼接:StringBuffer StringBuffer buf = new StringBuffer();

面试模拟

Q:String 为什么设计为不可变?

A:四个原因:1) 字符串常量池:不可变才能安全共享;2) HashMap key:不可变保证 hashCode 稳定;3) 安全性:类名、文件路径等不会被篡改;4) 线程安全:不可变对象天然线程安全。

Q:String 的 "+" 拼接和 StringBuilder 的 append 有什么区别?

A:单次 "+" 拼接编译器会优化为 StringBuilder,但循环中每次 "+" 都会创建新的 StringBuilder 对象,导致大量 GC。循环拼接必须显式使用 StringBuilder 复用同一个对象。另外常量拼接在编译期完成(常量折叠),不产生运行时开销。

Q:intern() 方法的作用是什么?

A:intern() 将字符串放入常量池并返回池中引用。如果池中已有相等字符串,直接返回池中引用;否则将该字符串加入池中。常用于节省内存:大量重复字符串通过 intern() 复用同一对象。但注意常量池在堆中(JDK 7+),过多 intern 也会占用堆内存。