String 不可变与 StringBuilder 选型
更新时间:2026-09-18。理解 String 的不可变性,才能解释字符串拼接的性能差异和一堆「看似奇怪」的现象。
一、String 为什么是不可变的
String 内部字符数组被 private final 修饰,且类本身 final 不可继承、不提供修改方法。s = s + "x" 不是改原对象,而是新建一个 String。
不可变的三大收益:
- 字符串常量池复用:相同字面量在常量池只有一份,节省内存、比较可快速命中。
- 安全:作为
HashMapkey、数据库连接串、文件路径等,值不会被意外篡改;多线程天然可共享。 - hashCode 可缓存:不可变所以 hash 只算一次,适合做哈希键。
java
String a = "ab"; // 常量池
String b = "a" + "b"; // 编译期常量折叠 → 也是 "ab"
System.out.println(a == b); // true(都指向常量池同一对象)二、拼接:+、StringBuilder、StringBuffer
| 方式 | 实现 | 适用 |
|---|---|---|
+(编译期常量) | 编译器折叠成一个字面量 | 字面量/常量拼接,放心用 + |
+(含变量、单次) | 编译器自动生成一个 StringBuilder | 一行内少量拼接,可读性优先用 + |
循环里 + | 每次迭代新建 StringBuilder 和 String,对象爆炸 | 必须手动用一个 StringBuilder |
StringBuilder | 可变、非线程安全、最快 | 单线程内大量拼接(默认选择) |
StringBuffer | 方法加 synchronized,线程安全但慢 | 极少需要(多线程共享拼接) |
java
// 反例:循环内 + ,每轮 new StringBuilder
String s = "";
for (int i = 0; i < 10000; i++) s += i;
// 正解:循环外建一次
StringBuilder sb = new StringBuilder(estimatedLen);
for (int i = 0; i < 10000; i++) sb.append(i);
String result = sb.toString();JDK 9 引入 invokedynamic(StringConcatFactory),运行期变量拼接不一定再显式 new StringBuilder,但循环里反复拼同一个累加变量仍应手动复用 StringBuilder。
三、常见坑
new String("x")主动进堆:绕过常量池复用,无必要别这么写。- 滥用
intern():JDK7+ 常量池在堆中且受 GC,但大量手工 intern 会增加维护成本,一般交给类加载的字面量。 - StringBuilder 不预设容量:底层 char 数组(JDK9 为 byte)会多次扩容拷贝,已知长度时传初始容量。
- 把 StringBuilder 当 key/跨方法共享修改:它可变,放进集合或多线程环境会引发内容变化问题。
- JDK9 压缩字符串:String 内部按是否含 Latin-1 用 byte[] 表示,节省内存,性能分析时注意版本差异。
总结
String 因 final 类加私有不可变字符数组而不可变,换来常量池复用、线程安全、可作安全参数和缓存 hashCode,任何“修改”都是新建对象;拼接时编译期常量和单行少量变量用 + 即可(编译器会折叠或自动用 StringBuilder),但循环中累加必须在循环外创建一个 StringBuilder 复用,否则每轮产生新对象造成对象爆炸,StringBuffer 只是在方法上加 synchronized 的线程安全版本、单线程下没必要用,使用 StringBuilder 时预估初始容量可避免内部数组扩容,并避免把可变的它当 HashMap key 或跨线程共享。