news 2026/10/5 16:56:34

一文讲透:String 为什么不可变?StringBuffer 与 StringBuilder 的区别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文讲透:String 为什么不可变?StringBuffer 与 StringBuilder 的区别

一文讲透:String 为什么不可变?StringBuffer 与 StringBuilder 的区别(图解)

字符串是开发和校招笔试中的常客:字符串转整形数字、字符串相加……面试中也频繁被问到String、StringBuffer 和 StringBuilder 之间的区别。本文用图解 + 源码的方式讲清楚三个问题:

  1. String 为什么不可修改?
  2. 哪两种类型是可以修改的?
  3. 这两种可修改的类型有什么区别?

目录

  • 一、String 为什么不可变?
    • 1.1 从源码看:final 类 + private final value 数组
    • 1.2 「不可变」的真正含义:所有修改都是创建新对象
    • 1.3 误区澄清:不是因为 final 所以内容不能改
    • 1.4 为什么要设计成不可变?(不可变对象的好处)
  • 二、想修改字符串怎么办?两种可修改的类型
    • 2.1 直接用 String 修改:效率非常低
    • 2.2 StringBuilder:内容可修改的字符串
    • 2.3 String 与 StringBuilder 的相互转换
  • 三、StringBuffer 与 StringBuilder 的区别(面试题)
  • 四、总结

一、String 为什么不可变?

1.1 从源码看:final 类 + private final value 数组

String 是一种不可变对象,字符串中的内容是不可改变的。这是因为String 类在设计时就是不可改变的。来看 JDK 1.8 中 String 类的部分实现——String 类中的字符实际保存在内部维护的value 字符数组中:

publicfinalclassStringimplementsjava.io.Serializable,Comparable<String>,CharSequence{/** * The value is used for character storage. */privatefinalchar[]value;// 字符真正存放的地方// ...提供大量读取 value 的方法,但没有任何修改 value 内容的方法}

从源码中可以读出三层设计意图:

  1. String 类被 final 修饰:表明该类不能被继承,防止子类重写行为破坏不可变性;
  2. value 被 private final 修饰:
    • private:外部访问不到value;
    • final:value 自身的值不能改变,即不能修改引用的指向,但是指向中的内容是可以修改的;
  3. 没有任何修改入口:String 类内部没有提供任何修改 value 内容的方法,所有 API(replace、substring、concat……)都只会返回新对象。

1.2 「不可变」的真正含义:所有修改都是创建新对象

所有涉及到可能修改字符串内容的操作,都是创建一个新对象、改变的是新对象,而原对象丝毫不动。

以最熟悉的s += " world"为例:

publicstaticvoidmain(String[]args){Strings="hello";s+=" world";// 并没有在原字符串上追加,而是产生了新对象System.out.println(s);// 输出:hello world}

所以,像下面这样的替换操作:

Stringstr="helloworld";System.out.println(str.replaceAll("l","_"));// he__owor_dSystem.out.println(str.replaceFirst("l","_"));// he_loworldSystem.out.println(str);// helloworld(原字符串纹丝不动)

注意事项:由于字符串是不可变对象,替换不会修改当前字符串,而是产生一个新的字符串。必须用变量接收返回值,否则"修改"毫无意义。

1.3 误区澄清:不是因为 final 所以内容不能改

网上有人说:字符串不可变是因为其内部保存字符的数组被 final 修饰了,因此不能改变——这种说法是错误的。

final修饰引用类型时,仅表明该引用变量不能引用其他对象(指向不能变),但其引用对象中的内容依然可以修改:

publicstaticvoidmain(String[]args){finalint[]array={1,2,3,4,5};array[0]=100;// 编译通过:内容可以修改System.out.println(Arrays.toString(array));// [100, 2, 3, 4, 5]// array = new int[]{4, 5, 6}; // 编译报错:无法为最终变量 array 分配值}

一句话总结:final 修饰类表明该类不想被继承;final 修饰引用类型表明该引用不能引用其他对象,但引用对象中的内容是可以修改的。
字符串真正不可变,是因为:类不能被继承 + value 拿不到也换不掉 + 类中没有任何修改内容的方法,所有 API 都返回新对象。

1.4 为什么要设计成不可变?(不可变对象的好处)

既然修改这么"麻烦",为什么 String 要设计成不可变的呢?

  1. 方便实现字符串对象池(字符串常量池):如果 String 可变,对象池就需要考虑写时拷贝的问题了——共享同一个对象时,一个引用的修改会波及所有使用者;
  2. 不可变对象是线程安全的:多线程可以放心共享,无需加锁;
  3. 不可变更方便缓存 hash code:作为 key 时可以更高效地保存到 HashMap 中。

二、想修改字符串怎么办?两种可修改的类型

2.1 直接用 String 修改:效率非常低

注意:尽量避免直接对 String 类型对象进行修改,因为 String 类是不能修改的,所有的修改都会创建新对象,效率非常低下。

publicstaticvoidmain(String[]args){longstart=System.currentTimeMillis();Strings="";for(inti=0;i<10000;++i){s+=i;// 每次循环都创建新对象}longend=System.currentTimeMillis();System.out.println(end-start);// 耗时很长start=System.currentTimeMillis();StringBuffersbf=newStringBuffer("");for(inti=0;i<10000;++i){sbf.append(i);// 原地修改,无新对象}end=System.currentTimeMillis();System.out.println(end-start);start=System.currentTimeMillis();StringBuildersbd=newStringBuilder();for(inti=0;i<10000;++i){sbd.append(i);// 原地修改,无新对象}end=System.currentTimeMillis();System.out.println(end-start);}

可以看到,在对 String 类进行修改时,效率是非常慢的(循环拼接 10000 次会产生约 10000 个临时对象)。因此:尽量避免对 String 的直接修改,如果要修改,建议尽量使用 StringBuffer 或者 StringBuilder。

2.2 StringBuilder:内容可修改的字符串

由于 String 的不可更改特性,为了方便字符串的修改,Java 中又提供了StringBuilder 和 StringBuffer两个类——这就是内容可以修改的两种类型。这两个类大部分功能是相同的,以 StringBuilder 为例(注意:不能直接赋值字符串,只能通过构造方法传入):

publicstaticvoidmain(String[]args){StringBuildersb1=newStringBuilder("hello");// 不能写 sb1 = "hello"StringBuildersb2=sb1;// 追加:即尾插 --> 字符、字符串、整形数字sb1.append(' ');// hellosb1.append("world");// hello worldsb1.append(123);// hello world123System.out.println(sb1);// hello world123System.out.println(sb1==sb2);// true(append 是原地修改,对象没变)System.out.println(sb1.charAt(0));// 获取 0 号位上的字符 hSystem.out.println(sb1.length());// 获取字符串的有效长度 14System.out.println(sb1.capacity());// 获取底层数组的总大小sb1.setCharAt(0,'H');// 设置任意位置的字符 -> Hello world123sb1.insert(0,"Hello world!!!");// 指定位置插入 -> Hello world!!!Hello world123System.out.println(sb1);System.out.println(sb1.indexOf("Hello"));// 获取 Hello 第一次出现的位置System.out.println(sb1.lastIndexOf("hello"));// 获取 hello 最后一次出现的位置sb1.deleteCharAt(0);// 删除首字符sb1.delete(0,5);// 删除 [0, 5) 范围内的字符Stringstr=sb1.substring(0,5);// 截取 [0, 5) 区间中的字符,以 String 方式返回System.out.println(str);sb1.reverse();// 字符串逆转str=sb1.toString();// 将 StringBuilder 以 String 方式返回System.out.println(str);}

StringBuilder 常用方法一览(作用 + 返回值):

方法作用返回值
StringBuilder append(String str)在尾部追加(相当于 String 的+=),可追加 boolean、char、char[]、double、float、int、long、Object、String 等返回自身(支持链式调用)
StringBuilder insert(int offset, String str)在 offset 位置插入八种基本类型 & String & Object 数据返回自身
StringBuilder deleteCharAt(int index)删除 index 位置的字符返回自身
StringBuilder delete(int start, int end)删除[start, end)区间内的字符返回自身
StringBuilder replace(int start, int end, String str)将[start, end)位置的字符替换为 str返回自身
StringBuilder reverse()反转字符串返回自身
char charAt(int index)获取 index 位置的字符返回该字符char
void setCharAt(int index, char ch)将 index 位置的字符设置为 ch无返回值
int indexOf(String str)返回 str 第一次出现的位置返回下标
int indexOf(String str, int fromIndex)从 fromIndex 位置开始查找 str 第一次出现的位置返回下标
int lastIndexOf(String str)返回 str 最后一次出现的位置返回下标
int lastIndexOf(String str, int fromIndex)从 fromIndex 开始找 str 最后一次出现的位置返回下标
String substring(int start)从 start 开始一直到末尾的字符以String方式返回(不改变自身)
String substring(int start, int end)将[start, end)范围内的字符以String方式返回(不改变自身)
int length()获取字符串的有效长度返回长度
int capacity()获取底层保存字符串空间总的大小返回容量
void ensureCapacity(int minimumCapacity)扩容无返回值
String toString()将所有字符按照 String 的方式返回返回String对象

从上述例子可以看出:String 和 StringBuilder 最大的区别在于 String 的内容无法修改,而 StringBuilder 的内容可以修改。频繁修改字符串的情况,考虑使用 StringBuilder。

2.3 String 与 StringBuilder 的相互转换

注意:String 和 StringBuilder 类不能直接转换。如果要想互相转换,可以采用如下原则:

  • String 变为 StringBuilder:利用 StringBuilder 的构造方法或append()方法;
  • StringBuilder 变为 String:调用toString()方法。
Strings="hello";// String -> StringBuilderStringBuildersb=newStringBuilder(s);// 方式一:构造方法sb.append(" world");// 方式二:append// StringBuilder -> StringStringret=sb.toString();

三、StringBuffer 与 StringBuilder 的区别(面试题)

String、StringBuffer、StringBuilder 的区别——这是一道高频面试题,核心区别如下:

  1. String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改;
  2. StringBuffer 与 StringBuilder 大部分功能是相似的;
  3. StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作。
对比维度StringStringBufferStringBuilder
内容可变性不可变(修改必产生新对象)可修改可修改
线程安全安全(不可变,天然安全)安全(方法加了同步处理)不安全(未做同步处理)
性能频繁修改时极低较高(同步有少许开销)最高
使用场景内容基本不变、作为常量共享多线程环境下频繁修改单线程环境下频繁修改(绝大多数场景,首选)

四、总结

  1. String 不可变的原因(源码级):
    • String 类被final修饰,不能被继承,防止子类破坏不可变性;
    • value 数组被private final修饰:外部访问不到、引用也不能换;
    • 类中没有任何修改 value 内容的方法,所有涉及修改的操作(replace、substring、+=……)都是创建新对象;
    • 注意误区:不是因为 final 导致内容不可变——final 修饰引用类型只是"指向不能变",内容本可以改;
  2. 不可变的好处:方便实现字符串常量池、线程安全、方便缓存 hash code;
  3. 两种可修改的类型:StringBuffer与StringBuilder,内容可直接原地修改,频繁修改字符串时用它们替代 String(append返回自身、支持链式调用,toString()转回 String);
  4. 两者的区别:StringBuffer 同步处理、线程安全;StringBuilder 未同步、线程不安全但效率最高——单线程首选 StringBuilder,多线程共享时选 StringBuffer;
  5. String 与 StringBuilder不能直接相互赋值/转换,只能通过构造方法 / append(String → StringBuilder)和toString(StringBuilder → String)。

如果这篇文章对你有帮助,欢迎点赞 👍 + 收藏 ⭐ + 关注,后续会继续更新 Java 系列文章!

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

口播智能体,AI短视频智能体开发流程(已开源)

一、先看整条流水线口播视频的生产&#xff0c;本质上是把一个重复劳动流程自动化&#xff1a;五个环节各用一件工具&#xff0c;全部开源或低成本&#xff1a;环节工具解决的问题① 文案提取faster-whisper&#xff08;OpenAI Whisper 的加速版&#xff09;把别人的爆款视频扒…

作者头像 李华
网站建设 2026/10/5 16:41:22

ComfyUI+SD1.5+ControlNet:AI线稿生成工作流搭建与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 16:40:28

NRF52832串口通信实战:从SDK配置到RTS/CTS硬件流控的完整排坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 16:40:04

Spirent TestCenter 实战:PPPoE、DHCP、QinQ 与 IGMP 配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 16:38:15

ADS1.2 安装与配置实战:在 Windows 10/11 上搭建 ARM 交叉编译环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 16:37:02

从零搭建openrig多相机阵列:同步触发与三维重建实战

做三维重建和体积视频的同行&#xff0c;这两年应该没少被“多相机阵列”这个词刷屏。NeRF、3D Gaussian Splatting火起来之后&#xff0c;大家发现单反绕着物体一圈圈拍虽然也能出结果&#xff0c;但效率低、运动对象没法拍、光照稍微一变重建质量就崩。于是圈子里开始频繁出现…

作者头像 李华