Java中的字符串拼接是非常常见的操作,不同的拼接方式背后涉及的对象创建逻辑差异很大,核心和字符串常量池、拼接的实现机制有关。下面我们通过具体的代码示例来逐一分析不同场景下的对象创建情况。

字面量直接拼接的场景
当拼接的双方都是字符串字面量时,编译器会在编译期就完成拼接优化,不会在运行时创建新的对象。我们看下面的代码:
public class StringConcatTest {
public static void main(String[] args) {
// 字面量直接拼接
String s1 = "hello" + "world";
String s2 = "helloworld";
// 输出true,说明s1和s2指向同一个常量池对象
System.out.println(s1 == s2);
}
}
这段代码中,<code>"hello" + "world"</code>在编译期就会被优化为<code>"helloworld"</code>,所以s1和s2都会指向字符串常量池中已有的<code>"helloworld"</code>对象,整个过程没有创建新的String对象,对象创建数量为0。
包含变量的字符串拼接场景
如果拼接的双方包含变量,编译器无法在编译期确定最终的结果,会在运行时通过<code>StringBuilder</code>来实现拼接,这时候就会涉及多个对象的创建。我们看下面的示例:
public class StringConcatTest2 {
public static void main(String[] args) {
String s1 = "hello";
String s2 = s1 + "world";
String s3 = "helloworld";
// 输出false,说明s2指向的是堆中的新对象
System.out.println(s2 == s3);
}
}
这段代码的执行过程如下:
- 首先在字符串常量池创建<code>"hello"</code>对象,s1指向它
- 执行<code>s1 + "world"</code>时,会先创建一个<code>StringBuilder</code>对象
- 然后调用<code>StringBuilder</code>的<code>append</code>方法拼接内容,之后调用<code>toString</code>方法,<code>toString</code>方法会创建一个新的String对象,这个对象存放在堆中,内容是<code>"helloworld"</code>
- 字符串常量池中原本就有<code>"helloworld"</code>对象,s3指向它
所以这个场景下,总共创建了2个新对象:1个<code>StringBuilder</code>对象和1个堆中的String对象。
使用StringBuilder主动拼接的场景
如果我们主动使用<code>StringBuilder</code>来拼接字符串,对象创建的数量会更可控,我们看下面的代码:
public class StringConcatTest3 {
public static void main(String[] args) {
String s1 = "hello";
StringBuilder sb = new StringBuilder();
sb.append(s1);
sb.append("world");
String s2 = sb.toString();
String s3 = "helloworld";
// 输出false,s2指向堆中的新String对象
System.out.println(s2 == s3);
}
}
这个场景的对象创建情况:
- 字符串常量池创建<code>"hello"</code>对象,s1指向它
- 创建1个<code>StringBuilder</code>对象
- 执行<code>sb.toString()</code>时,创建1个堆中的String对象,内容是<code>"helloworld"</code>
这里同样创建了2个新对象,和包含变量的拼接场景对象数量一致,但主动使用<code>StringBuilder</code>可以避免多次拼接时重复创建<code>StringBuilder</code>对象,性能更好。
拼接后调用intern方法的场景
如果拼接得到的String对象调用了<code>intern</code>方法,情况又会发生变化,我们看下面的示例:
public class StringConcatTest4 {
public static void main(String[] args) {
String s1 = "hello";
String s2 = s1 + "world";
// 调用intern方法,尝试将字符串放入常量池
String s3 = s2.intern();
String s4 = "helloworld";
// 输出true,说明s3和s4指向同一个常量池对象
System.out.println(s3 == s4);
}
}
这里<code>s2</code>是堆中的String对象,调用<code>intern</code>方法时,如果常量池中已经有<code>"helloworld"</code>,就会直接返回常量池中的对象引用,不会创建新对象。如果常量池中没有,就会把<code>s2</code>的引用放入常量池,同样不会创建新的String对象。所以这个场景下对象创建数量和之前包含变量的拼接场景一致,还是2个新对象。
不同场景对象数量总结
我们可以通过下面的表格来汇总不同拼接场景的对象创建数量:
| 拼接场景 | 新创建对象数量 | 说明 |
|---|---|---|
| 字面量直接拼接 | 0 | 编译期优化,直接使用常量池已有对象 |
| 包含变量的拼接 | 2 | 1个StringBuilder对象,1个堆中String对象 |
| 主动使用StringBuilder拼接 | 2 | 1个StringBuilder对象,1个堆中String对象 |
| 拼接后调用intern方法 | 2 | intern不会新增String对象,仅调整引用 |
在实际开发中,如果是少量的字符串拼接,使用<code>+</code>操作符即可,编译器会做一定的优化;如果是循环或者大量的字符串拼接,建议主动使用<code>StringBuilder</code>,避免重复创建<code>StringBuilder</code>对象带来的性能损耗。同时理解对象创建的数量,也能帮助我们更好地应对相关的面试问题,搞清楚字符串拼接背后的实现逻辑。