news 2026/4/15 22:54:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第二篇:String、StringBuilder、StringBuffer深度剖析
  1. 第一篇:Java基础概念四连问,==与equals、hashCode约定、接口vs抽象类、深拷贝vs浅拷贝
  2. 第二篇:String、StringBuilder、StringBuffer深度剖析

前言

在上一篇文章《Java基础概念四连问》中,我们学习了==equals()的区别、hashCode()equals()的约定等基础概念。但有一个类在Java开发中使用频率最高,却也最容易被误解——String

String a = "hello"String b = new String("hello")有什么区别?字符串拼接到底用+还是StringBuilderStringBufferStringBuilder谁更快?

这些问题不仅是面试高频题,更直接影响着你的代码性能和内存使用。今天,我们就来彻底揭开String家族的神秘面纱。读完本文,你将能回答:

  • 字符串常量池在JDK 7前后有什么变化?
  • intern()方法到底做了什么?
  • 为什么说String是不可变的?
  • StringBuilderStringBuffer的源码差异是什么?

下一篇,我们将进入集合框架的核心——HashMap源码深度剖析。


一、String的不可变性

1.1 源码验证

先看String类的源码(JDK 8):

publicfinalclassStringimplementsjava.io.Serializable,Comparable<String>,CharSequence{// 存储字符数组(final修饰,不可变)privatefinalcharvalue[];// 哈希码缓存privateinthash;// 构造函数publicString(Stringoriginal){this.value=original.value;this.hash=original.hash;}// 替换操作返回新String对象publicStringreplace(charoldChar,charnewChar){if(oldChar!=newChar){intlen=value.length;inti=-1;char[]val=value;while(++i<len){if(val[i]==oldChar){break;}}if(i<len){charbuf[]=newchar[len];for(intj=0;j<i;j++){buf[j]=val[j];}while(i<len){charc=val[i];buf[i]=(c==oldChar)?newChar:c;i++;}returnnewString(buf,true);// 返回新对象}}returnthis;}}

关键设计

  • final class:不能被继承
  • final char[] value:字符数组引用不可变(但数组内容可变?)
  • 所有修改操作(replacesubstringtoLowerCase等)都返回新String对象

1.2 为什么说String是不可变的?

虽然final char[] value只能保证引用地址不变,但数组内容理论上可以修改(通过反射)。然而,String类没有提供任何修改内部数组的方法,所有对外API都不会改变原字符串内容。

// 通过反射可以修改String内部值(证明不可变性是通过封装实现的)Stringstr="hello";Fieldfield=String.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)默认空间小,容易OOM
JDK 7堆(Heap)永久代空间不足,移到堆中
JDK 8+堆(Heap)元空间替代永久代,常量池仍在堆

2.2 两种创建方式的区别

// 方式1:字面量创建Strings1="hello";Strings2="hello";// 方式2:new创建Strings3=newString("hello");Strings4=newString("hello");System.out.println(s1==s2);// true,指向常量池同一对象System.out.println(s1==s3);// false,s3在堆中System.out.println(s3==s4);// false,两个不同的堆对象

内存图解

JDK 7+ 内存布局: ┌─────────────────────────────────────────────────────────────────────┐ │ 堆内存 │ ├─────────────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 字符串常量池 │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ "hello" │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 普通堆对象 │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ │ String │ │ String │ │ │ │ │ │ s3 │ │ s4 │ │ │ │ │ │ value ─┼───→│ value ─┼───→ 都指向常量池的"hello" │ │ │ │ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ 栈: ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ s1 │ │ s2 │ │ s3 │ │ s4 │ │ ↓ │ │ ↓ │ │ ↓ │ │ ↓ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │ │ ↓ ↓ ↓ ↓ 指向常量池 指向堆对象 指向堆对象

2.3 intern()方法详解

publicnativeStringintern();

作用:将字符串对象放入常量池,如果常量池中已有相同内容的字符串,则返回常量池中的引用。

// intern()示例Strings1=newString("hello");// 堆中对象Strings2=s1.intern();// 常量池对象Strings3="hello";// 常量池对象System.out.println(s1==s2);// false(堆 vs 常量池)System.out.println(s2==s3);// true(都是常量池对象)

2.4 JDK 6 vs JDK 7+ 的intern()差异

这是一个经典的面试陷阱:

// JDK 6Strings1=newString("a")+newString("b");// "ab"在堆中s1.intern();// 在永久代中创建"ab",并返回Strings2="ab";System.out.println(s1==s2);// false(堆 vs 永久代)// JDK 7+Strings1=newString("a")+newString("b");// "ab"在堆中s1.intern();// 常量池中直接存储堆中"ab"的引用Strings2="ab";System.out.println(s1==s2);// true!都指向堆中同一对象

JDK 7+的变化:常量池移到堆中后,intern()不再复制字符串内容,而是将堆中对象的引用存入常量池。


三、字符串拼接的编译器优化

3.1 编译期优化

// 源码Strings="hello"+" "+"world";// 编译后(javap -c)Strings="hello world";// 编译期直接拼接!

常量表达式(编译期可知的值)会在编译时直接拼接。

3.2 运行期优化

// 源码Strings1="hello";Strings2=s1+" world";// 编译后(JDK 5-8)Strings2=newStringBuilder().append(s1).append(" world").toString();// JDK 9+ 使用invokedynamic优化

注意:循环中使用+拼接会创建多个StringBuilder对象:

// 错误写法Stringresult="";for(inti=0;i<1000;i++){result+=i;// 每次循环都new StringBuilder}// 编译后等价于for(inti=0;i<1000;i++){result=newStringBuilder().append(result).append(i).toString();}// 创建了1000个StringBuilder对象和1000个String对象

3.3 正确写法

// 正确写法:显式使用StringBuilderStringBuildersb=newStringBuilder();for(inti=0;i<1000;i++){sb.append(i);}Stringresult=sb.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){toStringCache=null;super.append(str);returnthis;}publicsynchronizedStringtoString(){// 使用缓存优化toString性能if(toStringCache==null){toStringCache=Arrays.copyOfRange(value,0,count);}returnnewString(toStringCache,true);}}

4.4 性能对比

// 性能测试publicclassStringVsBuilderBenchmark{publicstaticvoidmain(String[]args){intiterations=100000;// String拼接(最慢)longstart=System.nanoTime();Strings="";for(inti=0;i<iterations;i++){s+=i;}longtime1=System.nanoTime()-start;// StringBuilder(最快)start=System.nanoTime();StringBuildersb=newStringBuilder();for(inti=0;i<iterations;i++){sb.append(i);}longtime2=System.nanoTime()-start;// StringBuffer(中等)start=System.nanoTime();StringBuffersbf=newStringBuffer();for(inti=0;i<iterations;i++){sbf.append(i);}longtime3=System.nanoTime()-start;System.out.println("String: "+time1/1000000+"ms");System.out.println("StringBuilder: "+time2/1000000+"ms");System.out.println("StringBuffer: "+time3/1000000+"ms");}}

典型输出(10万次拼接):

String: 28500ms (最慢,约28秒) StringBuilder: 8ms (最快) StringBuffer: 12ms (中等,略慢于StringBuilder)

4.5 使用场景总结

场景推荐原因
单线程字符串拼接StringBuilder性能最高,无锁开销
多线程共享可变字符串StringBuffer线程安全
简单的固定字符串拼接+(String)编译器会优化,代码简洁
循环中大量拼接StringBuilder避免创建大量临时对象
方法内局部变量拼接StringBuilder线程安全不需要同步

五、常见面试题

Q1:String为什么是不可变的?

:String类被声明为final,字符数组value被声明为final private,且没有提供任何修改内部状态的方法。这样设计的原因包括:字符串常量池可以安全共享、线程安全、哈希码可缓存、安全性(避免恶意修改)。

Q2:new String("hello")创建了几个对象?

:可能创建1个或2个对象:

  • 如果常量池中已有"hello",则只在堆中创建1个String对象
  • 如果常量池中没有"hello",则在常量池创建1个,堆中创建1个,共2个

Q3:StringBuilder和StringBuffer的区别?

  • StringBuilder:线程不安全,性能高,适合单线程场景
  • StringBuffer:线程安全(方法加synchronized),性能略低,适合多线程场景
  • 两者都继承自AbstractStringBuilder,底层都是可修改的char[]

Q4:String s = "a" + "b" + "c"创建了几个对象?

:只创建1个对象。编译器会优化为String s = "abc",常量池中创建"abc"

Q5:String s = a + b + c(a、b、c是变量)创建了几个对象?

:创建1个StringBuilder和1个结果String对象。编译后等价于:

Strings=newStringBuilder().append(a).append(b).append(c).toString();

Q6:intern()方法在JDK 6和JDK 7+有什么区别?

  • JDK 6:常量池在永久代,intern()会在永久代中复制一份字符串内容
  • JDK 7+:常量池在堆中,intern()将堆中对象的引用存入常量池,不复制内容

六、总结

6.1 核心要点

是否可变线程安全底层存储适用场景
String不可变安全final char[]字符串常量、作为Key
StringBuilder可变不安全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的源码,彻底搞懂哈希表的实现原理。


如果你觉得本文有帮助,欢迎点赞、评论、转发!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 22:54:10

从Scan Chain到ATPG:芯片DFT测试的自动化向量生成与故障诊断

1. 扫描链测试基础&#xff1a;从触发器到测试向量 想象一下你手里拿着一块刚下线的芯片&#xff0c;如何确保它内部数十亿个晶体管都正常工作&#xff1f;这就是扫描链&#xff08;Scan Chain&#xff09;技术的用武之地。简单来说&#xff0c;扫描链就像给芯片内部装了一条&q…

作者头像 李华
网站建设 2026/4/15 22:53:21

DuoPlus云手机更新:新增AI自动化功能、RPA一键刷机、API接口等

在这个日新月异的时代&#xff0c;每一个微小的变化都可能引领行业新潮流。DuoPlus基于不断创新的原则&#xff0c;在 3 月份对云手机进行了多项核心功能升级。 本次更新&#xff0c;不只是功能的增加&#xff0c;更是围绕“稳定性、效率与可规模化运营”的一次系统性优化。 …

作者头像 李华
网站建设 2026/4/15 22:46:39

安装阿帕奇maven的相关配置

首先到阿帕奇官网Welcome to Apache Maven – Maven下载maven 下载完成后进入文件夹&#xff0c;创建一个mvn_repo文件夹来当作本地仓库 进入conf文件夹内的settings.xml进行相关配置 首先配置本地仓库 然后下滑加一个阿里云镜像&#xff0c;下载依赖更快 <mirror><i…

作者头像 李华
网站建设 2026/4/15 22:45:26

Camera Tuning避坑指南:OB、坏点、网格噪点,这些“坑”你踩过几个?

Camera Tuning实战避坑手册&#xff1a;从OB校正到网格噪点的深度解析 在实验室昏暗的灯光下&#xff0c;调试工程师小李盯着屏幕上不断闪烁的紫色条纹&#xff0c;额头渗出细密的汗珠。这是本周第三次因为OB校正参数设置不当导致产线样片出现色偏&#xff0c;而明天就是客户验…

作者头像 李华
网站建设 2026/4/15 22:40:25

模型决策不透明正在拖垮AI落地,SITS2026最新共识:7类多模态解释失效陷阱,第5种90%团队仍在踩坑

第一章&#xff1a;模型决策不透明正在拖垮AI落地 2026奇点智能技术大会(https://ml-summit.org) 当金融风控系统拒绝一笔贷款申请却无法说明“为什么是78.3%而非79.1%的违约概率阈值触发否决”&#xff0c;当医疗影像模型标记肺结节为恶性却拒绝展示关键像素区域&#xff0c…

作者头像 李华