基础篇四String 真的不可变吗?三种字符串类到底该用哪个?

张开发
2026/4/19 23:40:29 15 分钟阅读

分享文章

基础篇四String 真的不可变吗?三种字符串类到底该用哪个?
文章目录一、先搞懂一个前提String 为什么不可变1. 字符串常量池的需要2. 线程安全3. hashCode 缓存二、三种字符串类全对比三、为什么拼接字符串性能差这么多String 拼接每次都创建新对象StringBuilder 拼接在原对象上追加性能实测对比四、StringBuilder 的扩容机制五、StringBuffer 线程安全体现在哪六、一个容易混淆的面试点七、实战选型速查八、面试速答模板个人网站每次拼接字符串JVM 都在偷偷创建新对象——你知道这件事吗Java 提供了三种字符串类选错一个轻则浪费内存重则拖垮性能。这篇文章用最通俗的方式讲清楚它们之间的区别帮你真正理解什么时候该用谁。一、先搞懂一个前提String 为什么不可变String被设计为不可变Immutable核心原因有三1. 字符串常量池的需要Strings1hello;Strings2hello;// s1 和 s2 指向常量池中同一个对象如果 String 可变s1 改了值s2 也会跟着变——这显然不是你想要的。不可变是字符串常量池存在的前提。2. 线程安全不可变对象天生线程安全不需要加锁多个线程可以放心共享同一个 String 对象。3. hashCode 缓存String 的 hashCode 只计算一次缓存在对象内部。如果字符串可变hashCode 就不可靠了HashMap 等数据结构会直接崩掉。// String 源码publicfinalclassString{privatefinalchar[]value;// JDK 8// private final byte[] value; // JDK 9用 byte[] 编码标志优化内存privateinthash;// 缓存 hashCode默认 0}final修饰类不可继承、final修饰数组引用不可变、private且不提供修改方法——三重保险保证不可变。二、三种字符串类全对比维度StringStringBuilderStringBuffer可变性不可变可变可变线程安全安全不可变不安全安全synchronized性能拼接时最低最高中等锁开销出现版本JDK 1.0JDK 1.5JDK 1.0底层结构final char[]/byte[]char[]/byte[]可扩容同 StringBuilder用一个生活类比来理解String→ 刻在石头上的字想改只能换一块石头StringBuilder→ 白板上的字随时擦写但白板只你一个人用StringBuffer→ 公告栏上的字随时擦写但每次写之前要锁门三、为什么拼接字符串性能差这么多String 拼接每次都创建新对象Stringsa;ssb;// 创建了新的 String 对象 abssc;// 又创建了新的 String 对象 abc// 表面上只拼了两次实际创建了 2 个中间对象 多个 StringBuilder拼接在编译后实际等价于StringsnewStringBuilder(a).append(b).append(c).toString();如果写在循环里每次循环都会创建一个新的 StringBuilder 对象极其浪费// ❌ 性能灾难Stringresult;for(inti0;i10000;i){resulti;// 每次循环 new StringBuilder new String}// 创建了约 10000 个 StringBuilder 10000 个 String// ✅ 正确写法StringBuildersbnewStringBuilder();for(inti0;i10000;i){sb.append(i);// 同一个 StringBuilder只扩容不新建}Stringresultsb.toString();StringBuilder 拼接在原对象上追加StringBuildersbnewStringBuilder(a);sb.append(b);// 在同一个对象上追加sb.append(c);// 还是在同一个对象上追加// 只创建了 1 个 StringBuilder 对象0 个中间 String性能实测对比// 拼接 10 万次耗时对比仅供参考具体数值因环境而异String→ 约5000msStringBuffer→ 约8msStringBuilder→ 约5ms差距 1000 倍这就是不可变 循环创建对象的代价。四、StringBuilder 的扩容机制StringBuilder 底层是一个数组容量不够时会自动扩容// 默认初始容量 16StringBuildersbnewStringBuilder();// 指定初始容量StringBuildersbnewStringBuilder(1024);扩容规则新容量 旧容量 × 2 2// 源码privateintnewCapacity(intminCapacity){intoldCapacityvalue.length;intnewCapacity(oldCapacity1)2;// 2 倍 2// ...}每次扩容都要创建新数组并拷贝数据所以如果能预估最终长度建议指定初始容量// ✅ 避免多次扩容StringBuildersbnewStringBuilder(10000);五、StringBuffer 线程安全体现在哪看源码就一目了然——所有公共方法都加了synchronized// StringBuffer 源码publicsynchronizedStringBufferappend(Stringstr){super.append(str);returnthis;}publicsynchronizedStringBufferdelete(intstart,intend){super.delete(start,end);returnthis;}publicsynchronizedStringtoString(){// ...}每个方法都加锁线程安全是保证了但单线程场景下就是纯纯的性能损耗。现实中 StringBuffer 用得越来越少了——因为字符串拼接绝大多数场景都是方法内的局部变量根本不存在多线程竞争用 StringBuilder 就够了。六、一个容易混淆的面试点// 下面两行代码有什么区别Strings1abc;Strings2newStringBuilder(a).append(b).append(c).toString();答案没有区别。编译器会优化纯字面量的拼接直接在编译期合并为一个字符串// 编译后 s1 等价于Strings1abc;// 不会创建 StringBuilder但如果拼接中包含变量就无法在编译期优化了Stringaa;Stringsabc;// 编译后等价于 new StringBuilder(a).append(b).append(c).toString()七、实战选型速查场景推荐原因少量拼接1~2 次String编译器优化代码简洁循环中拼接StringBuilder避免重复创建对象多线程共享拼接StringBuffer线程安全方法内局部变量拼接StringBuilder无竞争不需要同步配置项、常量String不可变天然安全JSON 构建StringBuilder单线程场景性能优先一句话口诀少量拼接用 String循环拼接用 Builder多线程共享用 Buffer。八、面试速答模板QString、StringBuilder、StringBuffer 的区别AString 不可变每次修改都会创建新对象StringBuilder 和 StringBuffer 可变在原对象上修改。StringBuilder 线程不安全但性能最高StringBuffer 通过 synchronized 保证线程安全但性能稍低。单线程场景优先 StringBuilder多线程共享场景用 StringBuffer少量拼接直接用 String。Q为什么 String 要设计成不可变A三个原因——① 支持字符串常量池多个引用可以安全共享同一个对象② 天然线程安全不需要同步开销③ hashCode 可以缓存提升作为 HashMap 键的性能。Q循环中用 拼接字符串有什么问题A每次都会创建一个新的 StringBuilder 对象拼接完再 toString 生成新的 String 对象。循环 N 次就创建 N 个 StringBuilder N 个中间 String造成严重的内存浪费和 GC 压力。应该在循环外创建一个 StringBuilder循环内用 append。相关文章原文阅读内容有帮助点赞、收藏、关注三连评论区等你

更多文章