Java 字符串拼接(附 4 种方法),新手避坑一次讲清

编程狮(w3cschool.cn) 2026-09-23 13:03:43 浏览数 (52)
反馈

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

Java 字符串拼接封面图

在正式开始之前,建议你先建立两个心智模型。第一个是"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 字符串拼接决策图

总结

回到开头那句话: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,二者是互补关系而非替代关系。

0 人点赞