字符串拼接(+)与StringBuffer/StringBuilder 对象创建数量对比(JDK7+)
要明确二者的对象创建数量,核心先区分字符串拼接的两种场景(编译期优化/运行期优化),再结合StringBuffer/StringBuilder原地修改的核心特性对比,同时要明确:对象包含String对象、StringBuilder/StringBuffer实例,仅toString()会生成最终String对象,以下按「场景拆解+数量计算+核心对比」的逻辑讲清,所有分析基于JDK7+(常量池在堆区,拼接底层优化为StringBuilder)。
核心前置原则
- String不可变:所有修改/拼接操作若需生成新字符串,必然创建新的String对象,无“原地修改”可能;
- StringBuffer/StringBuilder可变:底层维护非final的char[]/byte[],
append()等操作均为原地修改,不会创建任何临时String对象或自身新实例; - 字符串拼接
+是语法糖:JVM会根据拼接是否含「编译期可确定值」自动做优化,优化不同则对象数量天差地别; - 所有实际字符串对象(存储字符内容)均在堆区,常量池仅存堆对象引用,无新对象创建。
一、字符串拼接(+):分2大场景,对象数量差异极大
字符串拼接的对象创建数量,唯一判断标准:拼接表达式中是否包含普通变量/动态值(编译期无法确定最终值),分为「编译期优化」和「运行期优化」,二者对象数量完全不同。
场景1:编译期优化(仅含字面量/final常量,如"a"+"b"、final String s="a";s+"b")
核心逻辑
JVM编译时直接将拼接表达式合并为单个字符串字面量,等价于直接声明String str = "拼接结果",无任何临时对象创建。
对象创建数量:仅1个String对象
细节说明
- 拼接结果会按字面量创建规则执行:检查常量池,无则在堆创建1个String对象,将引用存入常量池;有则直接复用,无新对象;
- 全程无StringBuilder实例创建、无临时String对象,是拼接效率最高的场景,对象数量最少。
示例
// 等价于String s = "abc",仅创建1个String对象(堆中)
String s1 = "a" + "b" + "c";
// final变量是编译期常量,等价于"a"+"b",仅创建1个String对象
final String s2 = "a";
final String s3 = "b";
String s4 = s2 + s3;
场景2:运行期优化(含普通变量/动态值,如String s="a";s+"b"、i+""、方法返回值拼接)
核心逻辑
JVM编译时会将拼接语法糖自动替换为StringBuilder实现,等价于:new StringBuilder().append(拼接项1).append(拼接项2).toString(),仅单次拼接会按此规则执行。
对象创建数量:共2个对象(1个StringBuilder实例 + 1个最终String对象),0个临时String对象
细节说明
- 堆中创建1个StringBuilder实例:JVM自动new,用于执行原地append操作,无其他额外实例;
- 所有拼接项通过
append()原地修改,无任何临时String对象创建(这是JVM优化的核心,避免多次创建String); - 最终调用
toString()方法:在堆中创建1个新的String对象(存储拼接结果),常量池不会自动存入该对象的引用(需手动intern); - 拼接完成后,自动创建的StringBuilder实例会成为垃圾对象,等待GC回收。
示例
// 普通变量,编译期无法确定值,触发运行期优化
String s1 = "a";
String s2 = "b";
// 等价于new StringBuilder().append(s1).append(s2).toString()
// 创建:1个StringBuilder实例 + 1个String对象(结果"ab"),共2个对象
String s3 = s1 + s2;
特殊场景:循环中的字符串拼接(含普通变量,开发最易踩坑)
核心逻辑
JVM无法对循环中的多次拼接做整体优化,每次循环都会重新执行「运行期优化逻辑」:新建1个StringBuilder实例 + 执行append + 调用toString创建1个String对象。
对象创建数量:循环N次,创建 2*N 个对象(N个StringBuilder实例 + N个String对象)
细节说明
- 这是性能暴跌的根本原因:循环1000次就会创建2000个临时对象,循环10万次则创建20万个,导致GC频繁、内存冗余;
- 每次循环的StringBuilder和String对象都会成为垃圾,无任何复用可能。
示例
String str = "";
// 循环1000次,创建:1000个StringBuilder + 1000个String = 2000个对象
for (int i = 0; i < 1000; i++) {
str += i; // 每次循环都等价于new StringBuilder().append(str).append(i).toString()
}
二、StringBuffer/StringBuilder:固定2个对象(全程无临时对象)
StringBuffer和StringBuilder的对象创建数量与拼接次数无关(无论append多少次),仅与「是否手动创建实例」和「是否调用toString()」有关,核心得益于原地修改的特性。
核心逻辑
手动创建1个StringBuffer/StringBuilder实例,所有拼接操作通过append()原地修改底层数组,无任何临时对象,最终仅在调用toString()时创建1个最终的String对象。
对象创建数量:共2个对象(1个自身实例 + 1个最终String对象),0个临时对象
细节说明
- 堆中创建1个StringBuffer/StringBuilder实例:手动new,全程唯一,无论执行多少次
append()/insert(),都不会创建新的自身实例或临时String对象; - 多次
append()均为原地修改底层字符数组,容量不足时会触发扩容(创建新的char[]/byte[]复制原内容),但扩容仅创建数组,不创建任何对象(String/自身实例); - 最终调用
toString():在堆中创建1个新的String对象(存储最终拼接结果),无常量池自动入池; - StringBuffer与StringBuilder的对象创建数量完全一致,仅差
synchronized同步锁,与对象数量无关。
示例
// 1. 堆中创建1个StringBuilder实例(唯一)
StringBuilder sb = new StringBuilder();
// 2. 多次append:原地修改,0个对象创建(扩容也无新对象)
sb.append("a").append("b").append(123);
// 3. 调用toString:堆中创建1个String对象(结果"ab123")
String result = sb.toString();
// 总计:1个StringBuilder + 1个String = 2个对象
循环中的StringBuffer/StringBuilder:对象数量仍为2个(核心性能优势)
即使在循环中执行多次append(),仍仅创建2个对象,这是与字符串拼接+的核心性能差异:
// 1. 堆中创建1个StringBuilder实例(全程唯一)
StringBuilder sb = new StringBuilder();
// 循环1000次:仅原地append,0个新对象创建
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
// 2. 调用toString:创建1个String对象
String result = sb.toString();
// 总计:仍为1个StringBuilder + 1个String = 2个对象(与循环次数无关)
三、核心对比表:所有场景对象创建数量汇总
为直观对比,按开发常用场景分类,明确各场景下的对象类型+数量,对比场景为「拼接生成1个最终字符串」:
| 操作方式 | 具体场景 | 创建的对象(类型+数量) | 核心特点 |
|---|---|---|---|
字符串拼接+ |
编译期优化(字面量/final常量) | 1个String对象 | 无临时对象,效率最高,常量池复用 |
字符串拼接+ |
运行期优化(普通变量,单次拼接) | 1个StringBuilder + 1个String(共2个) | 无临时String对象,单次拼接效率尚可 |
字符串拼接+ |
运行期优化(普通变量,循环N次) | N个StringBuilder + N个String(共2*N个) | 临时对象暴增,GC频繁,性能极低 |
| StringBuffer | 单次/循环多次append(最终toString) | 1个StringBuffer + 1个String(共2个) | 原地修改,无临时对象,线程安全,效率中等 |
| StringBuilder | 单次/循环多次append(最终toString) | 1个StringBuilder + 1个String(共2个) | 原地修改,无临时对象,非线程安全,效率最高 |
四、关键补充:易混淆的细节说明
1. 为什么运行期拼接+和StringBuilder手动调用,对象数量相同但循环性能天差地别?
- 单次运行期拼接
+:JVM自动创建1个StringBuilder,对象数量与手动调用一致,性能几乎无差异; - 循环拼接
+:每次循环都新建1个StringBuilder,2*N个临时对象会导致「对象创建/销毁开销」+「GC开销」,而手动调用仅1个StringBuilder,无额外开销。
2. 扩容会创建新对象吗?
不会。StringBuffer/StringBuilder扩容时,仅会在堆中创建新的char[]/byte[]数组(用于存储更多字符),并将原数组内容复制到新数组,不会创建任何String对象或自身的新实例,数组是“数据结构”,并非JVM中的“对象实例”(对象特指类的实例)。
3. toString()方法的作用:必创1个String对象
StringBuffer/StringBuilder的toString()是生成最终字符串的唯一方式,该方法会在堆中创建1个新的String对象,将底层字符数组的内容复制到新String对象中,这一步是必须的,也是二者唯一创建String对象的环节。
4. 常量池会自动存入拼接/append的结果吗?
不会(除非手动调用intern())。
- 编译期拼接的结果:会按字面量规则入池,常量池存其引用;
- 运行期拼接
+、StringBuffer/StringBuilder的toString()结果:均为堆中普通String对象,常量池不会自动存入其引用,需手动调用result.intern()才能将引用入池实现复用。
五、核心结论+开发选择建议
1. 核心对象数量结论
- 字符串拼接
+:仅编译期优化场景创建1个String对象,其余场景均会创建StringBuilder临时实例,循环中临时对象暴增; - StringBuffer/StringBuilder:所有场景均仅创建2个对象(自身实例+最终String),与拼接/循环次数无关,无任何临时对象。
2. 日常开发选择建议
- 编译期拼接场景(字面量/final常量):直接用
+,简洁高效,仅1个String对象; - 单次运行期拼接(普通变量,如s1+"b"+s2):用
+或StringBuilder均可,对象数量相同,性能无差异,优先选+(语法简洁); - 循环拼接/大数据量拼接(日志、报文、集合拼接):强制使用StringBuffer/StringBuilder,避免2*N个临时对象,单线程选StringBuilder(最高效),多线程选StringBuffer(线程安全);
- 指定初始容量:使用StringBuffer/StringBuilder时,根据预估拼接长度指定初始容量(如
new StringBuilder(1000)),避免扩容的数组复制开销,进一步提升性能。

浙公网安备 33010602011771号