面试官问String、StringBuilder、StringBuffer有什么区别附图解性能对比避坑指南摘要String 不可变每次修改创建新对象StringBuilder 可变、线程不安全、性能最高StringBuffer 可变、线程安全synchronized、性能次之。本文用“三种写字工具”比喻 性能实测数据 JVM 底层分析逃逸分析/GC 2026 现代 Java 写法Text Blocks/Records 工具库推荐彻底讲透这道面试必考题。 系列导航上一篇面试官问和equals()有什么区别为什么重写equals必须重写hashCode下一篇预告面试官问final、finally、finalize有什么区别全部85题目录点击查看 面试还原面试官String、StringBuilder、StringBuffer 有什么区别什么场景下用哪个这是 Java 面试中出场率最高的基础题之一。看似简单但面试官可以一路追问到“字符串常量池”、“编译期优化”、“JVM 逃逸分析”、“JDK 9 底层实现变化”。今天用一张图 三种写字工具比喻 性能实测数据 JVM 底层原理让你彻底掌握并应对追问。金句记忆String 不变Builder 快Buffer 安全。单线程拼装用 Builder多线程安全用 Buffer少量操作用 String。 String、StringBuilder、StringBuffer一图看懂 生活比喻三种写字工具想象你需要写一份很长的作业String 钢笔写错了不能擦只能换一张新纸从头写。每修改一次就换一张新纸产生新对象。→ 适合不常变化的字符串如配置信息、常量、方法返回值。StringBuilder 铅笔 橡皮写错了就擦掉重写速度飞快。但旁边的人可能抢你的笔线程不安全。→ 适合单线程下大量拼接如循环内拼接 JSON、SQL、日志。StringBuffer 铅笔 橡皮 保险柜每次用笔都要开锁用完锁上安全但慢方法加 synchronized。→ 适合多线程环境下的字符串操作如缓存构建、多线程日志聚合。 关键对比表维度StringStringBuilderStringBuffer可变性不可变final char[]/byte[]可变可变线程安全安全天然不可变不安全安全synchronized 方法级锁粒度较粗性能最慢每次操作 new 对象最快次快有锁开销适用场景少量操作或常量单线程大量拼接多线程大量拼接存储位置常量池字面量或堆new堆堆继承关系Object 子类AbstractStringBuilder 子类AbstractStringBuilder 子类默认容量—字面量直接存储16无参构造16无参构造扩容机制—旧容量 * 2 2旧容量 * 2 2记忆口诀常变用 Builder多线用 Buffer不变用 String。 面试官追问重点追问1String 为什么设计成不可变四点核心答字符串常量池如果 String 可变一个引用修改会影响其他引用破坏常量池设计。哈希码缓存String 的hashCode只计算一次并缓存可变会导致哈希值变化影响 HashMap 等集合。线程安全天然不可变无需同步。类加载器安全类名通常用字符串表示可变会导致安全漏洞。追问2String s new String(abc)创建了几个对象答如果常量池中没有abc则创建2 个对象常量池一个 堆一个。如果常量池中已有则只创建1 个堆对象。可通过javap -c查看字节码验证。追问3循环内拼接用和StringBuilder.append()哪个好答单行拼接编译器自动优化成StringBuilder两者性能相同。循环内拼接用每次循环会创建新的StringBuilder对象效率低且 GC 压力大。务必用StringBuilder。// ❌ 差每次循环 new StringBuilderStringresult;for(inti0;i10000;i){resulti;// 等价于 result new StringBuilder(result).append(i).toString()}// ✅ 好一个 StringBuilder 复用StringBuildersbnewStringBuilder();for(inti0;i10000;i){sb.append(i);}追问4JVM 的“逃逸分析”能优化字符串拼接吗答逃逸分析JVM 的 JIT 编译器会分析对象是否在方法外可见。标量替换如果对象没有逃逸JIT 会将其拆解为基本类型标量直接在栈上分配避免堆分配和 GC 压力。局限性对于循环内的字符串拼接如result i逃逸分析通常无法优化因为每次迭代都创建新对象并逃逸出当前作用域赋值给循环外变量。结论不要把性能优化完全寄托于 JVM代码层面仍需使用StringBuilder。追问5JDK 9 之后 String 底层有什么变化答JDK 8 及以前private final char[] value;每个字符占 2 字节。JDK 9private final byte[] value;coder字段Latin-1 占 1 字节UTF-16 占 2 字节。目的节省内存。大部分字符串是 Latin-1英文字符用byte[]可节省约 50% 内存。这项优化被称作Compact Strings紧凑字符串。 2026 年的 String 新写法随着 Java 17/21 LTS 的普及以下特性已成为开发标配1. Text BlocksJDK 15 正式当需要拼接大段 SQL、JSON 或 HTML 时Text Blocks 是首选无需StringBuilder的append链// ❌ 旧写法难以阅读Stringjson{\name\:\张三\,\age\:25,\city\:\北京\};// ✅ Text Blocks清晰易读Stringjson { name: 张三, age: 25, city: 北京 } ;适用场景SQL 拼接、JSON 构造、模板生成等。Text Blocks 能极大提升代码可读性。2. RecordsJDK 14 正式JDK 16 完善在 2026 年Records 已是数据载体类的首选。它天然支持 String 的不可变性理念// ❌ 旧写法classUser{privatefinalStringname;privatefinalintage;// 构造器 getter equals hashCode toString}// ✅ Records自动生成一切天然不可变recordUser(Stringname,intage){}Records 的字段是final的与 String 的不可变性一脉相承。 工具库推荐除了原生StringBuilder还有更优雅的选择1. StringJoinerJDK 8当需要带分隔符的拼接时如 CSVStringJoiner比StringBuilder更语义化StringJoinerjoinernewStringJoiner(, ,[,]);joiner.add(甲).add(乙).add(丙);System.out.println(joiner);// [甲, 乙, 丙]优点自动处理首尾分隔符无需手动判断isFirst。2. Guava JoinerGoogle Guava处理集合拼接时更加优雅importcom.google.common.base.Joiner;ListStringlistArrays.asList(甲,乙,丙);StringresultJoiner.on(, ).join(list);// 甲, 乙, 丙// 处理 nullJoiner.on(, ).skipNulls().join(list);优点支持skipNulls、useForNull对 null 友好。 常见坑点坑1字符串拼接 的编译期优化不等于运行时优化// 编译期常量折叠 → 直接变成 hello worldStrings1hello world;// 优化为 hello world// 运行时拼接 → 使用 StringBuilderStrings2hello;Strings3s2 world;// 底层 new StringBuilder().append(s2).append(world).toString()注意只有编译期可知的常量表达式才会被折叠变量拼接不会。坑2StringBuilder 的初始容量设置不当导致频繁扩容StringBuildersbnewStringBuilder();// 默认容量 16for(inti0;i100000;i){sb.append(i);}// 频繁扩容旧容量 * 2 2每次扩容需要复制原数组影响性能正确如果能预估最终长度指定初始容量StringBuildersbnewStringBuilder(100000);// 预分配足够空间坑3多线程环境下误用 StringBuilder// 多线程环境 ❌StringBuildersbnewStringBuilder();// 多个线程同时 sb.append() → 数据错乱、丢失正确使用StringBuffer或显式加锁。但StringBuffer的锁粒度是方法级竞争激烈。高并发下可考虑ThreadLocal或ConcurrentLinkedQueue等无锁方案。坑4认为 StringBuilder 的toString()会复制数组StringBuildersbnewStringBuilder(hello);Stringssb.toString();// JDK 8复制新数组JDK 9共享 value不可变引用JDK 8 及以前toString()会new String(value, 0, count)复制数组保护性复制。JDK 9Compact StringsString构造器接收byte[]时会先检查coder并可能共享数组引用进一步优化性能。 可运行验证代码importjava.util.StringJoiner;importjava.util.Arrays;publicclassStringVsBuilderVsBuffer{publicstaticvoidmain(String[]args){// 1. 性能对比intiterations100000;// String 循环拼接longstart1System.currentTimeMillis();Strings;for(inti0;iiterations;i){si;}longend1System.currentTimeMillis();System.out.println(String 拼接耗时: (end1-start1)ms);// StringBuilderlongstart2System.currentTimeMillis();StringBuildersbnewStringBuilder(iterations*5);for(inti0;iiterations;i){sb.append(i);}longend2System.currentTimeMillis();System.out.println(StringBuilder 耗时: (end2-start2)ms);// StringBufferlongstart3System.currentTimeMillis();StringBufferbuffernewStringBuffer(iterations*5);for(inti0;iiterations;i){buffer.append(i);}longend3System.currentTimeMillis();System.out.println(StringBuffer 耗时: (end3-start3)ms);// 2. 验证不可变性Stringstrhello;Stringstr2str world;System.out.println(str 是否被修改? str);// 还是 hello// 3. 编译期常量折叠验证Stringahello world;Stringbhello world;System.out.println(常量折叠: (ab));// true// 4. StringJoiner 示例StringJoinerjoinernewStringJoiner(, ,[,]);joiner.add(甲).add(乙).add(丙);System.out.println(StringJoiner: joiner);// [甲, 乙, 丙]}}典型输出100000 次String 拼接耗时: 15423ms StringBuilder 耗时: 7ms StringBuffer 耗时: 9ms str 是否被修改? hello 常量折叠: true StringJoiner: [甲, 乙, 丙]❓ 评论区挑战问题下面代码共创建了几个对象不考虑常量池已有的情况Stringsabc;A. 1 个B. 2 个C. 3 个D. 5 个面试官问和equals()有什么区别为什么重写equals必须重写hashCode 评论区挑战问题下面代码的输出是什么为什么Strings1newString(java);Strings2newString(java);System.out.println(s1s2);System.out.println(s1.equals(s2));A. true / falseB. false / trueC. false / falseD. true / true✅ 答案公布正确答案B. false / true解析s1 s2两个对象都是new创建的在堆中不同地址 → false。s1.equals(s2)String 重写了equals()比较字符序列 → true。 系列导航上一篇面试官问和equals()有什么区别为什么重写equals必须重写hashCode下一篇预告面试官问final、finally、finalize有什么区别全部85题目录点击查看你在实际开发中遇到过因为字符串拼接性能问题导致的线上事故吗或者见过哪些“优雅”的字符串拼接写法欢迎评论区分享你的故事。