Java 中的 char、String、StringBuilder 与 StringBuffer 深度详解
(2025–2026 面试/生产最常考点 + 实际使用建议)
1. 四者最核心对比表(背下来)
| 特性 | char | String | StringBuilder | StringBuffer |
|---|---|---|---|---|
| 基本类型 / 引用类型 | 基本类型 | 引用类型(不可变对象) | 引用类型(可变字符序列) | 引用类型(可变字符序列) |
| 是否线程安全 | — | 是(不可变天然安全) | 否 | 是(方法加 synchronized) |
| 是否可变 | 不可变(值类型) | 不可变 | 可变 | 可变 |
| 底层数据结构 | 16位 Unicode 字符 | char[] + final | char[](非 final) | char[](非 final) |
| 性能(单线程拼接) | — | 最差 | 最好 | 较差 |
| 性能(多线程拼接) | — | 好(但不适合频繁修改) | 最差(需手动加锁) | 较好(内置锁) |
| 内存开销 | 极小 | 较大(每次修改产生新对象) | 中等 | 中等(比 Builder 略大) |
| 典型使用场景 | 单个字符、switch case | 常量、键值、配置文件 | 单线程高频拼接 | 多线程高频拼接(极少用) |
| JDK 引入版本 | 1.0 | 1.0 | 1.5 | 1.0 |
| 现代推荐指数(2025+) | 正常使用 | ★★★★☆(大部分场景) | ★★★★★(推荐) | ★☆☆☆☆(基本不推荐) |
2. char 的关键知识点(常被忽略)
- 本质:16 位无符号整数(0 ~ 65535),对应 Unicode 码点
- char vs Character:char 是基本类型,Character 是包装类(有缓存 -128~127)
- 常见陷阱:
char c1 = 'A'; // 65
char c2 = 65; // 合法,等价于上面
char c3 = '\u0041'; // 合法,Unicode 转义
// 经典面试题
System.out.println('a' + 'b'); // 195 (char 自动提升为 int 相加)
System.out.println("" + 'a' + 'b'); // "ab"(字符串拼接)
- Java 9+ 字符串底层压缩:如果字符串只包含 Latin-1 字符(0-255),则用 byte[] 存储(节省一半内存),但 char 本身仍是 2 字节。
3. String 的不可变性(Immutable)核心原理
为什么 String 是 final + 不可变?
- 安全性(ClassLoader、反射、常量池)
- 线程安全(天然)
- HashMap 等集合的 key 稳定性
- 字符串常量池(String Pool)复用
内存结构(JDK 8 → JDK 17+ 变化)
JDK 8及之前:
String 对象
↓
private final char value[]; // 指向字符数组
private final int hash; // 缓存 hashCode
JDK 9+(Compact Strings):
String 对象
↓
private final byte[] value; // 可能是 byte[] 或 char[]
private final byte coder; // 0 = LATIN1, 1 = UTF16
经典面试题:下面代码创建了几个 String 对象?
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello").intern();
String s5 = s1 + "world"; // 新对象
String s6 = "hello" + "world"; // 编译期优化为 "helloworld"(常量池)
答案:
- s1、s2 → 1 个(常量池)
- s3 → 2 个(new + 常量池)
- s4 → 1 个(intern 强制入池)
- s5 → 至少 2 个(”world” + 拼接结果)
- s6 → 1 个(编译期直接 “helloworld”)
4. StringBuilder vs StringBuffer 终极对比
共同点:
- 都继承 AbstractStringBuilder
- 内部都是 char[](JDK9+ 是 byte[] + coder)
- 扩容机制:newCapacity = oldCapacity * 2 + 2(JDK8+ 优化)
最大区别:线程安全实现方式
// StringBuffer(几乎所有 public 方法都加了 synchronized)
public synchronized StringBuffer append(char c) { ... }
// StringBuilder(无锁)
public StringBuilder append(char c) { ... }
真实性能对比(2025 常见压测结论)
| 场景 | String | StringBuilder | StringBuffer | 推荐选择 |
|---|---|---|---|---|
| 单线程 10万次 append | 最慢(~500ms) | 最快(~15ms) | 中等(~80ms) | StringBuilder |
| 多线程 每个线程 1万次 | 安全但慢 | 不安全(数据错乱) | 安全但较慢 | StringBuffer 或 Concurrent 替代 |
| 现代微服务日志拼接 | — | 首选 | 几乎不用 | StringBuilder + 局部 synchronized |
StringBuffer 现代替代方案(2025+ 推荐)
// 方案1:局部加锁(最常用)
StringBuilder sb = new StringBuilder();
synchronized (lock) {
sb.append(...).append(...);
}
// 方案2:StringJoiner(JDK8+,更优雅)
StringJoiner joiner = new StringJoiner(",", "[", "]");
joiner.add("a").add("b");
String result = joiner.toString();
// 方案3:Collectors.joining()(Stream 场景)
String result = list.stream().collect(Collectors.joining(", ", "[", "]"));
5. 面试/生产中最常问的 10 个深度问题
- String a = “hello” + “world” 和 String b = a + “!” 底层发生了什么?
- new String(“hello”) 到底创建了几个对象?
- 为什么 String 的 hashCode 要缓存?
- StringBuilder 在扩容时容量是怎么计算的?
- StringBuilder 和 StringBuffer 的 char[] 什么时候从 char[] 变成 byte[]?
- intern() 方法在 JDK 6、7、8+ 的行为变化?
- 字符串常量池在 JDK 7+ 迁移到堆后有什么影响?
- 如何高效拼接大量字符串(百万级)?
- String 是线程安全的吗?在什么场景下不安全?
- StringBuilder.append() 为什么不线程安全?底层指令重排序可能导致什么问题?
6. 2025–2026 生产最佳实践总结
| 场景 | 推荐工具 | 理由 / 注意事项 |
|---|---|---|
| 常量、配置、日志模板 | String | 不可变 + 常量池复用 |
| 单线程、循环中频繁拼接 | StringBuilder | 性能最高 |
| 多线程共享一个 builder(极少见) | StringBuffer 或 加锁 | 优先加锁而非 StringBuffer |
| Stream / Collectors 拼接 | StringJoiner / joining | 代码最优雅 |
| 需要线程安全且高性能 | StringBuilder + 局部锁 或 ThreadLocal | 避免全局锁开销 |
| JSON / 模板引擎输出 | StringBuilder | 大部分框架内部也这么做 |
一句话总结(面试背诵版):
char 是单个字符,String 是不可变字符串常量,StringBuilder 是单线程高性能可变字符序列,StringBuffer 是历史遗留的多线程安全版本(现已被 StringBuilder + 局部同步取代)。
需要我针对某个具体问题展开更详细的字节码分析、内存图、压测代码、JDK 版本差异对比吗?
可以直接告诉我你最想深挖的点。