第二篇:String、StringBuilder、StringBuffer深度剖析

张开发
2026/4/15 22:54:19 15 分钟阅读

分享文章

第二篇:String、StringBuilder、StringBuffer深度剖析
第一篇Java基础概念四连问与equals、hashCode约定、接口vs抽象类、深拷贝vs浅拷贝第二篇String、StringBuilder、StringBuffer深度剖析前言在上一篇文章《Java基础概念四连问》中我们学习了与equals()的区别、hashCode()与equals()的约定等基础概念。但有一个类在Java开发中使用频率最高却也最容易被误解——String。String a hello和String b new String(hello)有什么区别字符串拼接到底用还是StringBuilderStringBuffer和StringBuilder谁更快这些问题不仅是面试高频题更直接影响着你的代码性能和内存使用。今天我们就来彻底揭开String家族的神秘面纱。读完本文你将能回答字符串常量池在JDK 7前后有什么变化intern()方法到底做了什么为什么说String是不可变的StringBuilder和StringBuffer的源码差异是什么下一篇我们将进入集合框架的核心——HashMap源码深度剖析。一、String的不可变性1.1 源码验证先看String类的源码JDK 8publicfinalclassStringimplementsjava.io.Serializable,ComparableString,CharSequence{// 存储字符数组final修饰不可变privatefinalcharvalue[];// 哈希码缓存privateinthash;// 构造函数publicString(Stringoriginal){this.valueoriginal.value;this.hashoriginal.hash;}// 替换操作返回新String对象publicStringreplace(charoldChar,charnewChar){if(oldChar!newChar){intlenvalue.length;inti-1;char[]valvalue;while(ilen){if(val[i]oldChar){break;}}if(ilen){charbuf[]newchar[len];for(intj0;ji;j){buf[j]val[j];}while(ilen){charcval[i];buf[i](coldChar)?newChar:c;i;}returnnewString(buf,true);// 返回新对象}}returnthis;}}关键设计final class不能被继承final char[] value字符数组引用不可变但数组内容可变所有修改操作replace、substring、toLowerCase等都返回新String对象1.2 为什么说String是不可变的虽然final char[] value只能保证引用地址不变但数组内容理论上可以修改通过反射。然而String类没有提供任何修改内部数组的方法所有对外API都不会改变原字符串内容。// 通过反射可以修改String内部值证明不可变性是通过封装实现的Stringstrhello;FieldfieldString.class.getDeclaredField(value);field.setAccessible(true);char[]value(char[])field.get(str);value[0]H;System.out.println(str);// Hello结论String的不可变性是通过封装实现的而非绝对的物理不可变。1.3 为什么要设计成不可变原因说明字符串常量池只有不可变才能安全地共享否则一个引用修改会影响所有线程安全不可变对象天然线程安全无需同步哈希码缓存哈希码只需计算一次可作为HashMap的Key安全避免被恶意修改如文件路径、数据库URL等二、字符串常量池2.1 常量池的位置演进字符串常量池是理解String内存行为的关键。JDK版本常量池位置原因JDK 6及之前永久代PermGen默认空间小容易OOMJDK 7堆Heap永久代空间不足移到堆中JDK 8堆Heap元空间替代永久代常量池仍在堆2.2 两种创建方式的区别// 方式1字面量创建Strings1hello;Strings2hello;// 方式2new创建Strings3newString(hello);Strings4newString(hello);System.out.println(s1s2);// true指向常量池同一对象System.out.println(s1s3);// falses3在堆中System.out.println(s3s4);// false两个不同的堆对象内存图解JDK 7 内存布局 ┌─────────────────────────────────────────────────────────────────────┐ │ 堆内存 │ ├─────────────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 字符串常量池 │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ hello │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 普通堆对象 │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ │ String │ │ String │ │ │ │ │ │ s3 │ │ s4 │ │ │ │ │ │ value ─┼───→│ value ─┼───→ 都指向常量池的hello │ │ │ │ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ 栈 ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ s1 │ │ s2 │ │ s3 │ │ s4 │ │ ↓ │ │ ↓ │ │ ↓ │ │ ↓ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │ │ ↓ ↓ ↓ ↓ 指向常量池 指向堆对象 指向堆对象2.3 intern()方法详解publicnativeStringintern();作用将字符串对象放入常量池如果常量池中已有相同内容的字符串则返回常量池中的引用。// intern()示例Strings1newString(hello);// 堆中对象Strings2s1.intern();// 常量池对象Strings3hello;// 常量池对象System.out.println(s1s2);// false堆 vs 常量池System.out.println(s2s3);// true都是常量池对象2.4 JDK 6 vs JDK 7 的intern()差异这是一个经典的面试陷阱// JDK 6Strings1newString(a)newString(b);// ab在堆中s1.intern();// 在永久代中创建ab并返回Strings2ab;System.out.println(s1s2);// false堆 vs 永久代// JDK 7Strings1newString(a)newString(b);// ab在堆中s1.intern();// 常量池中直接存储堆中ab的引用Strings2ab;System.out.println(s1s2);// true都指向堆中同一对象JDK 7的变化常量池移到堆中后intern()不再复制字符串内容而是将堆中对象的引用存入常量池。三、字符串拼接的编译器优化3.1 编译期优化// 源码Stringshello world;// 编译后javap -cStringshello world;// 编译期直接拼接常量表达式编译期可知的值会在编译时直接拼接。3.2 运行期优化// 源码Strings1hello;Strings2s1 world;// 编译后JDK 5-8Strings2newStringBuilder().append(s1).append( world).toString();// JDK 9 使用invokedynamic优化注意循环中使用拼接会创建多个StringBuilder对象// 错误写法Stringresult;for(inti0;i1000;i){resulti;// 每次循环都new StringBuilder}// 编译后等价于for(inti0;i1000;i){resultnewStringBuilder().append(result).append(i).toString();}// 创建了1000个StringBuilder对象和1000个String对象3.3 正确写法// 正确写法显式使用StringBuilderStringBuildersbnewStringBuilder();for(inti0;i1000;i){sb.append(i);}Stringresultsb.toString();四、StringBuilder与StringBuffer源码对比4.1 继承体系┌─────────────────────────────────────────────────────────────────────┐ │ AbstractStringBuilder │ │ 可变字符序列的抽象父类 │ │ ├─ char[] value // 存储字符非final可修改 │ │ └─ int count // 已使用长度 │ └─────────────────────────────────────────────────────────────────────┘ ↑ ┌───────────────┴───────────────┐ │ │ ┌─────────────────┐ ┌─────────────────┐ │ StringBuilder │ │ StringBuffer │ │ 线程不安全 │ │ 线程安全 │ │ 无同步 │ │ 所有方法加锁 │ └─────────────────┘ └─────────────────┘4.2 StringBuilder源码publicfinalclassStringBuilderextendsAbstractStringBuilderimplementsjava.io.Serializable,CharSequence{// 无锁性能高publicStringBuilderappend(Stringstr){super.append(str);returnthis;}}4.3 StringBuffer源码publicfinalclassStringBufferextendsAbstractStringBuilderimplementsjava.io.Serializable,CharSequence{// 所有public方法都加了synchronizedpublicsynchronizedStringBufferappend(Stringstr){toStringCachenull;super.append(str);returnthis;}publicsynchronizedStringtoString(){// 使用缓存优化toString性能if(toStringCachenull){toStringCacheArrays.copyOfRange(value,0,count);}returnnewString(toStringCache,true);}}4.4 性能对比// 性能测试publicclassStringVsBuilderBenchmark{publicstaticvoidmain(String[]args){intiterations100000;// String拼接最慢longstartSystem.nanoTime();Strings;for(inti0;iiterations;i){si;}longtime1System.nanoTime()-start;// StringBuilder最快startSystem.nanoTime();StringBuildersbnewStringBuilder();for(inti0;iiterations;i){sb.append(i);}longtime2System.nanoTime()-start;// StringBuffer中等startSystem.nanoTime();StringBuffersbfnewStringBuffer();for(inti0;iiterations;i){sbf.append(i);}longtime3System.nanoTime()-start;System.out.println(String: time1/1000000ms);System.out.println(StringBuilder: time2/1000000ms);System.out.println(StringBuffer: time3/1000000ms);}}典型输出10万次拼接String: 28500ms 最慢约28秒 StringBuilder: 8ms 最快 StringBuffer: 12ms 中等略慢于StringBuilder4.5 使用场景总结场景推荐原因单线程字符串拼接StringBuilder性能最高无锁开销多线程共享可变字符串StringBuffer线程安全简单的固定字符串拼接String编译器会优化代码简洁循环中大量拼接StringBuilder避免创建大量临时对象方法内局部变量拼接StringBuilder线程安全不需要同步五、常见面试题Q1String为什么是不可变的答String类被声明为final字符数组value被声明为final private且没有提供任何修改内部状态的方法。这样设计的原因包括字符串常量池可以安全共享、线程安全、哈希码可缓存、安全性避免恶意修改。Q2new String(hello)创建了几个对象答可能创建1个或2个对象如果常量池中已有hello则只在堆中创建1个String对象如果常量池中没有hello则在常量池创建1个堆中创建1个共2个Q3StringBuilder和StringBuffer的区别答StringBuilder线程不安全性能高适合单线程场景StringBuffer线程安全方法加synchronized性能略低适合多线程场景两者都继承自AbstractStringBuilder底层都是可修改的char[]Q4String s a b c创建了几个对象答只创建1个对象。编译器会优化为String s abc常量池中创建abc。Q5String s a b ca、b、c是变量创建了几个对象答创建1个StringBuilder和1个结果String对象。编译后等价于StringsnewStringBuilder().append(a).append(b).append(c).toString();Q6intern()方法在JDK 6和JDK 7有什么区别答JDK 6常量池在永久代intern()会在永久代中复制一份字符串内容JDK 7常量池在堆中intern()将堆中对象的引用存入常量池不复制内容六、总结6.1 核心要点类是否可变线程安全底层存储适用场景String不可变安全final char[]字符串常量、作为KeyStringBuilder可变不安全char[]单线程大量拼接StringBuffer可变安全同步char[]多线程共享拼接6.2 字符串常量池演进速记JDK 6永久代 → 空间小容易OOM JDK 7移到堆 → 空间更大intern()存引用 JDK 8仍在堆 → 元空间独立常量池不变6.3 性能最佳实践单次拼接 → 用String编译器优化 循环拼接 → 用StringBuilder显式创建 多线程拼接 → 用StringBuffer或用StringBuilder加外部锁6.4 面试金句如果面试官问你“String、StringBuilder、StringBuffer的区别”你可以这样回答“String是不可变类底层是final char[]所有修改操作都返回新对象适合作为常量或HashMap的Key。StringBuilder和StringBuffer都是可变字符序列底层是可修改的char[]。StringBuilder线程不安全但性能最高适合单线程大量拼接StringBuffer所有public方法都加了synchronized线程安全但性能略低适合多线程共享。字符串常量池在JDK 7后移到堆中intern()方法不再复制字符串内容而是存储堆中对象的引用这减少了内存开销。”下篇预告理解了String家族的底层原理我们掌握了Java中最常用的数据结构。但Java集合框架中还有一个使用频率极高的类——HashMap。它为什么能实现O(1)的查找JDK 8为什么要引入红黑树多线程下为什么不能用HashMap下一篇《HashMap源码深度剖析——从JDK 7到JDK 8的演进》将带你深入HashMap的源码彻底搞懂哈希表的实现原理。如果你觉得本文有帮助欢迎点赞、评论、转发

更多文章