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