java字符串拼接 到底该怎么选?先给结论:少量拼接用 + 或 StringBuilder,循环/大量拼接必须用 StringBuilder,避免 String 不可变带来的性能浪费。这是本文最重要的判断标准,请记在心里。本文会系统地讲清楚 java字符串拼接 的 4 种主流方法:加号与 concat、StringBuilder 与 StringBuffer、String.join、String.format,并配上方法选择表和常见排错表,帮你一次把坑踩平。

在正式开始之前,建议你先建立两个心智模型。第一个是"String 不可变":在 Java 里,String 对象一旦创建,它内部的字符内容就不能再被修改,所谓"拼接"本质上都是创建新对象。第二个是"缓冲区":StringBuilder 提前申请一块可以伸缩的字符缓冲区,往里追加内容时不需要每次都新建对象,这正是它在循环里快的根本原因。理解了这两点,后面的 4 种方式你就能一眼看出适用边界,而不是死记硬背。
一、先看结论:Java 字符串拼接怎么选
为什么需要先给一张选择表?因为 java字符串拼接 的方法虽多,但每种都有明确的"主场"。新手最容易犯的错,就是把一种写法用在所有场景里:用加号拼循环、用 format 拼海量日志、用 concat 拼可能为 null 的字段。结果要么性能拉胯,要么出现莫名其妙的 "null" 或乱码。先把方法、适用场景、不推荐场景、推荐顺序摆出来,后面每一节再展开讲原理和代码。
下面这张「方法选择表」按"推荐顺序"从高到低排列。注意,推荐顺序不是绝对排名,而是"在它的主场里优先用"。比如 String.join 只在"集合/数组 + 固定分隔符"这个场景最强,放在别的地方就不合适。
| 方式 | 适用场景 | 不推荐场景 | 推荐顺序 |
|---|---|---|---|
| + 运算符 | 少量(三五段)、一次性、可读性优先的拼接 | 循环内拼接、成百上千段内容 | 3 |
| String.concat | 两个明确非 null 的字符串直接相接 | 多段拼接、含 null、数量不固定 | 4 |
| StringBuilder | 循环拼接、动态组装、SQL/JSON 拼装、日志收集 | 只拼一两段(小题大做) | 1 |
| StringBuffer | 多线程共享同一个拼接缓冲 | 单线程场景(有同步开销) | 2(限多线程) |
| String.join | 用固定分隔符把集合/数组拼成一条字符串 | 需要复杂格式或占位符替换 | 2(限集合场景) |
| String.format | 需要占位符、日期/数字格式化输出 | 高频循环内大量拼接(相对慢) | 2(限格式化场景) |
从"Java 字符串性能"的角度看,结论非常清晰:可控、可复用的拼接走 StringBuilder;不可控、一次性的小拼接走加号;结构化集合走 join;格式化输出走 format。把握住这个主逻辑,你就不会再为"该用哪个"纠结。
如果你是从零开始学 Java,建议先跟着 Java 教程 把基础语法跑通,再回头看本文的拼接坑;日常写代码时也可以随时翻 Java 速查手册查方法签名。
二、环境与最小示例
要跑通本文示例,你只需要一个最基础的 Java 环境:JDK 8 及以上均可(推荐 JDK 11 或 JDK 17,长期支持版本更稳)。不需要任何第三方依赖,用 javac 编译、java 运行即可。下面给出最小拼接示例,目的不是炫技,而是让你亲眼看到"加号拼接"这件事本身在做什么。
// 文件名:ConcatDemo.java
public class ConcatDemo {
public static void main(String[] args) {
String a = "Hello"; // 定义第一个字符串字面量,存入常量池
String b = "World"; // 定义第二个字符串字面量
String c = a + b; // 用 + 拼接;因 String 不可变,底层会新建 String 对象存放结果
System.out.println(c); // 打印拼接结果,验证拼接是否成功
}
}
(以下为说明性代码,未在本机执行;预期结果:控制台输出 HelloWorld)
这里的关键点是"String 不可变原理"。在 Java 中,String 内部用一个字符数组存储内容,而这个数组被 final 修饰且对外不暴露修改入口。也就是说,字符串一旦创建,它的内容就锁死了。当你写 a + b 时,虚拟机并不是去"修改"a 或 b,而是开辟一块新的内存,把 a 和 b 的字符依次拷进去,生成一个全新的 String 对象 c。原来的 a、b 如果没有其他引用,就静静等待垃圾回收。
理解不可变有两个好处。第一,你不会再误以为"拼接会改掉原字符串"——原字符串始终没动。第二,你能预判性能代价:拼得越多、循环越深,新建对象的次数就呈线性甚至更差的增长,这正是大量拼接必须用 StringBuilder 的根本原因。下一节我们对比加号与 concat 的边界,你会更直观。建议在本地运行示例验证拼接结果,检查输出是否符合预期,配置好字符集避免中文乱码。
三、方式一:+ 运算符与 concat
加号是最直观的拼接方式,几乎所有 Java 教材的第一行字符串代码都是它。它的优点是人话级可读性:"姓名:" + name 一眼就懂。但要注意,编译器对加号的处理是有"智能"的:在简单的一次性拼接里,现代 Java(Java 9+ 通过 StringConcatFactory)会把连续的加号优化成高效的字节码,并不真的每次都 new 对象;可一旦加号出现在循环体里,每次迭代都是一次独立拼接,优化就失效了,性能会明显下滑。
concat 是 String 类自带的方法,语义上等价于"把另一个字符串接到自己后面"。它比加号更"老实":只做两段拼接,不处理复杂的混合类型;而且当任一参数为 null 时行为不同——加号会把 null 转成字符串 "null",而 concat 在调用方为 null 时会直接抛 NullPointerException,需要你提前判空。
// 文件名:PlusAndConcat.java
public class PlusAndConcat {
public static void main(String[] args) {
String s1 = "编程"; // 第一段内容
String s2 = "狮"; // 第二段内容
String s3 = s1 + s2; // 用 + 运算符拼接,最直观
String s4 = s1.concat(s2); // 用 concat 方法拼接,效果同上
String s5 = s1.concat(s2).concat("!"); // 链式 concat,连续拼接多段
System.out.println(s3); // 输出:编程狮
System.out.println(s4); // 输出:编程狮
System.out.println(s5); // 输出:编程狮!
}
}
(以下为说明性代码,未在本机执行;预期结果:依次输出 编程狮、编程狮、编程狮!)
适用边界总结:当你只是把两三个固定片段接起来、且性能不在关键路径上时,加号完全够用,别过度设计;String concat 适合"明确两段、都非 null"的干净场景。但凡是"循环里拼"或"片段数量不定",就请果断切换到下一节的 StringBuilder。需要提醒的是,在讨论 java字符串拼接 时,加号和 concat 都属于"不可变拼接"阵营,性能上限是一样的——它们都逃不开新建对象。
四、方式二:StringBuilder 与 StringBuffer
这是本文的重头戏,也是"循环/大量拼接必须用 StringBuilder"这句话的真正落点。StringBuilder 内部维护一个可自动扩容的字符数组(缓冲区),你调用 append 时,字符被直接写进缓冲区,几乎不新建对象;只有缓冲区满时才会扩容一次。因此无论你拼 10 段还是 1 万段,成本都摊得很平,这就是它快的本质。
StringBuffer 与 StringBuilder 的 API 几乎一模一样,区别在于 StringBuffer 的每个公开方法都加了 synchronized 关键字,保证多线程同时操作同一缓冲区时不会数据错乱。代价是同步带来的性能开销。所以在单线程(占绝大多数业务场景)里,直接用 StringBuilder 即可;只有当你确实把同一个拼接缓冲交给多个线程共享时,才考虑 StringBuffer。
// 文件名:StringBuilderDemo.java
import java.util.List;
import java.util.ArrayList;
public class StringBuilderDemo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder(); // 创建可变字符串缓冲区,默认初始容量 16
sb.append("Hello"); // 向缓冲区追加第一段内容
sb.append(" "); // 追加一个空格作为分隔
sb.append("World"); // 追加第二段内容
String result = sb.toString(); // 拼完后再转成不可变 String 返回
System.out.println(result); // 输出:Hello World
// 下面演示循环拼接:这正是 StringBuilder 的主场
List<String> words = new ArrayList<>();
words.add("Java"); words.add("Python"); words.add("Go");
StringBuilder loopSb = new StringBuilder();
for (String w : words) { // 遍历集合,逐段追加
loopSb.append(w).append(","); // 链式 append,循环里也不会频繁新建对象
}
System.out.println(loopSb); // 输出:Java,Python,Go, (末尾多一个逗号,可手动处理)
}
}
(以下为说明性代码,未在本机执行;预期结果:第一行输出 Hello World,第二行输出 Java,Python,Go,)
关于"Java 字符串性能",这里补充两个实用细节。其一,如果你能预估最终长度,可以在构造时传入容量,例如 new StringBuilder(1024),这样可以减少扩容次数,进一步提速。其二,append 支持几乎所有类型(int、double、char、Object 等),它会自动把参数转成字符写入缓冲区,省去你手动转换的麻烦。记住:凡是字符串拼接方式里涉及"循环"或"长度不固定",StringBuilder 就是默认答案。
五、方式三/四:String.join 与 String.format 及排错
前面两种是"通用拼接",而 String.join 与 String.format 属于"场景化拼接",各自有不可替代的主场。String.join 专门解决"把一堆元素用同一个分隔符连起来"的需求,比如把列表拼成用逗号分隔的一行。它语义清晰、不易出错,比手写循环追加分隔符优雅得多。String.format 则解决"带占位符的格式化输出",比如把数字、日期按指定样式塞进模板里,%s 代表字符串、%d 代表整数、%.2f 代表保留两位小数的浮点数。
// 文件名:JoinAndFormat.java
import java.util.Arrays;
import java.util.List;
public class JoinAndFormat {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "Python", "Go"); // 准备待拼接的元素集合
String joined = String.join(", ", list); // 第二个参数是分隔符,把集合元素连成一条
System.out.println(joined); // 输出:Java, Python, Go
String fmt = String.format("语言:%s,排名:%d", "Java", 1); // %s 填字符串,%d 填整数
System.out.println(fmt); // 输出:语言:Java,排名:1
// 也可以用静态导入后的 printf 直接打印格式化结果
System.out.printf("占比:%.1f%%
", 73.5); // %.1f 保留一位小数,%% 输出百分号
}
}
(以下为说明性代码,未在本机执行;预期结果:依次输出 Java, Python, Go、语言:Java,排名:1、占比:73.5%)
使用这两种方式时,也有一些高频踩坑点。下面是本文的「排错表」,覆盖 null 拼接、忘记 toString、中文乱码、占位符不匹配四类典型问题,建议收藏对照。
| 现象 | 常见原因 | 修复方向 |
|---|---|---|
| 拼接结果里出现 "null" 字样 | 某段内容为 null,加号或 concat 会把 null 转成字符串 "null" 接上去 | 拼接前判空,或用 Objects.toString(obj, "") 把 null 转成空串 |
打印出来是 StringBuilder@1b6d3586 这类内容 |
忘记调用 toString(),直接打印/返回了缓冲区对象本身 |
在最后一步调用 sb.toString() 再返回或输出 |
中文变成乱码(如 ??? 或 锘?) |
读写时编码不一致,用默认编码读入 UTF-8 文本 | 显式指定 StandardCharsets.UTF_8,如 new String(bytes, StandardCharsets.UTF_8) |
String.format 抛 MissingFormatArgumentException |
占位符数量与后面实参数量/类型不匹配 | 核对 %s、%d 等占位符与参数一一对应,类型也要匹配 |

总结
回到开头那句话:java字符串拼接 的核心,是理解 String 的不可变,以及据此选择"新建对象"还是"复用缓冲区"。加号与 concat 属于不可变拼接,适合少量、一次性的干净场景;StringBuilder 用缓冲区复用征服循环与海量拼接,是性能首选;StringBuffer 在多线程共享缓冲时才考虑;String.join 与 String.format 则是集合连接、格式化输出这两个专属场景的最优解。
把"Java 字符串性能"放进日常考量,你会发现很多慢代码只是因为用错了拼接方式:把 for 循环里的加号换成 StringBuilder,往往就能立竿见影。同时也别走向另一个极端——只为拼两三个固定片段就去 new 一个 StringBuilder,反而让代码变啰嗦。真正的高手,是看场景下菜,而不是死守一条规则。最后提醒,遇到诡异的 "null"、乱码或 format 报错,先翻上面的排错表,基本都能对号入座。想查方法签名也可翻 Java 速查手册。
延伸学习
常见问题
Q:为什么在 for 循环里用加号拼接字符串会很慢?
因为 String 是不可变的,每次循环里的 s = s + item 都会创建一个全新的 String 对象,把旧内容拷进去再加上新片段,旧对象随即变成垃圾。循环一万次,就新建上万个对象,既费内存又触发频繁 GC。改用 StringBuilder 的 append,字符直接写进同一块缓冲区,几乎不新建对象,性能差距在大数据量下可达数十倍。
Q:StringBuilder 和 StringBuffer 到底该用哪个?
绝大多数业务代码是单线程的,直接用 StringBuilder 即可,它没有同步开销、速度更快。只有当多个线程会同时操作"同一个"拼接缓冲区时,才用 StringBuffer——它的方法加了 synchronized,能保证线程安全。注意:若是每个线程各自 new 一个 StringBuilder,那本来就不存在竞争,依然用 StringBuilder。
Q:String.format 和 StringBuilder 拼接,哪个更好?
看目的。如果你要的是"占位符 + 格式化"(比如 %d、%.2f、日期样式),String.format 表达力最强、可读性最好,少量使用完全没问题;但如果在高频循环里反复 format,它比 StringBuilder 慢,应改用手动 append。简单说:格式化输出选 format,大量/循环拼接选 StringBuilder,二者是互补关系而非替代关系。

TRAE-AI编程



