String、StringBuilder 和 StringBuffer 的区别?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,主要想考察以下几个层面的理解:
- 对字符串核心特性的理解:面试官不仅想知道它们 “可变或不可变” 的结论,更想考察你是否理解 “不可变对象” 的设计意图、内存影响(如字符串常量池)及其带来的线程安全性。
- 对线程安全概念的掌握及应用能力:能否清晰阐述三者线程安全性的差异,并理解其实现原理(如
StringBuffer的synchronized关键字),以及这对性能产生的具体影响。 - 性能分析与场景选型能力:是否能结合实际开发场景(例如循环拼接字符串、高并发字符串处理),分析三者性能差异的根源,并给出合理的选择依据和最佳实践。
- API 熟悉程度:虽然问题聚焦区别,但了解它们的主要 API(如
append,toString)及设计模式(如 Builder 模式)也是加分项。
核心答案
最核心的区别在于 可变性 与 线程安全性。
- String:不可变字符序列。任何修改操作都会生成新的 String 对象。由于其不可变性,它是线程安全的。
- StringBuilder(JDK 5+引入):可变字符序列。提供高效的字符串修改操作。非线程安全,但在单线程环境下性能最高。
- StringBuffer:可变字符序列。功能与
StringBuilder类似,但关键方法是synchronized修饰的,因此是线程安全的,但同步会带来额外的性能开销。
简单来说:需要字符串常量或少量操作时用 String;在单线程环境下进行大量字符串拼接或修改时,优先使用 StringBuilder;必须在多线程环境下进行字符串修改时,才使用 StringBuffer。
深度解析
原理/机制
-
String 的不可变性:
String类内部使用final char[](JDK 9 后为final byte[])存储数据。final使得该引用不可指向新数组,且类没有暴露任何修改此数组内容的方法。这种设计带来了诸多好处:- 安全性:作为参数传递时,不用担心被意外修改(如用作
HashMap的 key)。 - 缓存 HashCode:
String的hashCode()方法会缓存第一次计算的结果,因为值永不变,这提升了像HashMap这类集合的性能。 - 实现字符串常量池:JVM 可以池化相同的字符串字面量,节省内存。
- 安全性:作为参数传递时,不用担心被意外修改(如用作
-
StringBuilder 与 StringBuffer 的可变性:两者都继承自
AbstractStringBuilder,内部维护一个可变的字符数组char[] value。进行append或insert等操作时,直接修改该数组的内容,仅在数组容量不足时进行扩容(通常是翻倍),避免了String每次修改都创建新对象的开销。 -
线程安全实现:
StringBuffer通过在几乎所有公开方法上添加synchronized关键字来实现线程安全。而StringBuilder则没有此修饰,因此在多线程并发修改时,可能导致数据不一致。
代码示例
// 1. String 的“修改”代价:产生大量中间对象
String str = “Hello”;
for (int i = 0; i < 1000; i++) {
// 每次循环都会 new 一个新的 String 对象,效率低下
str = str + “World”;
}
// 2. StringBuilder 的高效操作(单线程场景)
StringBuilder sb = new StringBuilder(“Hello”);
for (int i = 0; i < 1000; i++) {
// 始终在同一个 StringBuilder 对象内操作
sb.append(“World”);
}
String result = sb.toString(); // 只在最后生成一个 String 对象
// 3. StringBuffer 的线程安全操作(多线程场景,但现代开发有更好选择)
StringBuffer sbf = new StringBuffer();
// 多个线程可以安全地调用 sbf.append(...),但会因锁竞争影响性能
对比分析与最佳实践
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 是(天然) | 否 | 是(synchronized 实现) |
| 性能 | 修改操作最差(大量对象创建) | 单线程下最高 | 低于 StringBuilder(有锁开销) |
| 适用场景 | 字符串常量、少量操作、作为哈希键 | 单线程下大量字符串操作 | 多线程下大量字符串操作(已较少使用) |
最佳实践与误区:
- 无脑使用 StringBuilder:在单线程、明确需要频繁修改字符串的场景(如循环体内拼接、动态生成 SQL/JSON 字符串),应优先使用
StringBuilder。 - 不要用
+号在循环中拼接字符串:这是最常见的性能陷阱,应使用StringBuilder替代。 StringBuffer的现代替代品:由于synchronized是粗粒度锁,在高并发下性能不佳。现代 Java 并发编程中,若需线程安全的字符串拼接,更推荐使用ThreadLocal为每个线程分配一个StringBuilder,或使用无锁类如java.util.concurrent包下的工具,而非直接使用StringBuffer。- 局部变量原则:
StringBuilder通常应作为方法内的局部变量使用。由于其非线程安全,将其作为共享的成员变量是危险的。
总结
理解 String(不可变、安全)、StringBuilder(可变、高效、非线程安全)和 StringBuffer(可变、线程安全、性能有损耗)的核心区别,关键在于结合 可变性、线程安全和性能 这三个维度,并根据实际的开发场景做出最合理的选择。在当代 Java 开发中,StringBuilder 已成为字符串构建的首选工具。