一文讲透:String 为什么不可变?StringBuffer 与 StringBuilder 的区别(图解)
字符串是开发和校招笔试中的常客:字符串转整形数字、字符串相加……面试中也频繁被问到String、StringBuffer 和 StringBuilder 之间的区别。本文用图解 + 源码的方式讲清楚三个问题:
- String 为什么不可修改?
- 哪两种类型是可以修改的?
- 这两种可修改的类型有什么区别?
目录
- 一、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 内容的方法}从源码中可以读出三层设计意图:
- String 类被 final 修饰:表明该类不能被继承,防止子类重写行为破坏不可变性;
- value 被 private final 修饰:
private:外部访问不到value;final:value 自身的值不能改变,即不能修改引用的指向,但是指向中的内容是可以修改的;
- 没有任何修改入口: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 要设计成不可变的呢?
- 方便实现字符串对象池(字符串常量池):如果 String 可变,对象池就需要考虑写时拷贝的问题了——共享同一个对象时,一个引用的修改会波及所有使用者;
- 不可变对象是线程安全的:多线程可以放心共享,无需加锁;
- 不可变更方便缓存 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 的区别——这是一道高频面试题,核心区别如下:
- String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改;
- StringBuffer 与 StringBuilder 大部分功能是相似的;
- StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作。
| 对比维度 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 内容可变性 | 不可变(修改必产生新对象) | 可修改 | 可修改 |
| 线程安全 | 安全(不可变,天然安全) | 安全(方法加了同步处理) | 不安全(未做同步处理) |
| 性能 | 频繁修改时极低 | 较高(同步有少许开销) | 最高 |
| 使用场景 | 内容基本不变、作为常量共享 | 多线程环境下频繁修改 | 单线程环境下频繁修改(绝大多数场景,首选) |
四、总结
- String 不可变的原因(源码级):
- String 类被final修饰,不能被继承,防止子类破坏不可变性;
- value 数组被private final修饰:外部访问不到、引用也不能换;
- 类中没有任何修改 value 内容的方法,所有涉及修改的操作(
replace、substring、+=……)都是创建新对象; - 注意误区:不是因为 final 导致内容不可变——final 修饰引用类型只是"指向不能变",内容本可以改;
- 不可变的好处:方便实现字符串常量池、线程安全、方便缓存 hash code;
- 两种可修改的类型:StringBuffer与StringBuilder,内容可直接原地修改,频繁修改字符串时用它们替代 String(
append返回自身、支持链式调用,toString()转回 String); - 两者的区别:StringBuffer 同步处理、线程安全;StringBuilder 未同步、线程不安全但效率最高——单线程首选 StringBuilder,多线程共享时选 StringBuffer;
- String 与 StringBuilder不能直接相互赋值/转换,只能通过构造方法 / append(String → StringBuilder)和toString(StringBuilder → String)。
如果这篇文章对你有帮助,欢迎点赞 👍 + 收藏 ⭐ + 关注,后续会继续更新 Java 系列文章!