
Java 基础:String不可变性、String vs StringBuffer vs StringBuilder、常量池、intern()方法
一、String 不可变性(核心本质)
1. 定义
String 一旦创建,内容无法修改,任何修改字符串的操作都会生成新字符串,原字符串不变。
2. 底层源码原因
String底层使用 private final char[] value 存储字符final修饰数组引用:数组地址不可改private修饰:外部无法直接修改数组元素- 无对外提供修改数组的方法,彻底锁死内容
3. 常见误区
s += "xxx"不是修改原字符串,是新建对象replace()、substring()、trim()全都返回新 String- 不可变 ≠ 引用不可变:
String s="a";s="b"只是引用指向改变
4. 不可变性四大优势
- 线程安全:只读不可改,多线程并发无冲突
- 常量池复用:节省内存,相同字符串只存一份
- 哈希值固定:可安全作为 HashMap 键
- 数据安全:参数传递、网络传输不会被篡改
二、String / StringBuffer / StringBuilder 三者终极对比
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全 | 安全(synchronized) | 不安全 |
| 效率 | 极低(频繁拼接大量创建新对象) | 中等 | 最高 |
| 底层 | final char[] | 可变 char[] 扩容 | 可变 char[] 扩容 |
| 适用场景 | 少量字符串定义、常量 | 多线程拼接 | 单线程大量拼接 |
| 继承关系 | 独立类 | 继承 AbstractStringBuilder | 继承 AbstractStringBuilder |
核心执行逻辑
- String:拼接=新建对象,海量拼接极度耗内存
- StringBuffer:方法加锁,并发安全,加锁损耗性能
- StringBuilder:无锁,单线程最快,并发会数据错乱
开发使用规范
- 日常字符串赋值、常量:用 String
- 单线程循环拼接、日志、SQL拼接:优先 StringBuilder
- 多线程共享字符串拼接:只能用 StringBuffer
三、字符串常量池(String Pool)
1. 常量池位置
- JDK1.6 及之前:方法区永久代
- JDK1.7+:移至堆内存(重点考点)
2. 常量池作用
缓存字面量字符串,实现字符串复用,减少重复对象创建,节省堆内存。
3. 两种创建字符串方式(重中之重)
方式1:字面量创建 String s1 = "abc"
- 先去字符串常量池查找
- 存在:直接返回池中引用
- 不存在:在常量池创建对象,返回引用
- 只创建 1 个对象
方式2:new 创建 String s2 = new String("abc")
- 先查常量池有无
"abc" - 无:常量池创建1个
- 堆内存强制再新建1个 String 对象
- 最少创建 2 个对象
4. 相等判断规则
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
s1 == s2:true 同一常量池引用s1 == s3:false 池内对象 != 堆new对象s1.equals(s3):true 比较内容
5. 编译期优化
常量拼接直接在编译期合并放入常量池
String a = "a"+"b"; // 编译直接变成 "ab" 进常量池
变量拼接编译期无法优化,走堆对象
String x = "a";
String b = x + "b"; // 堆中新对象,不入常量池
四、intern() 方法 全网最全解析
1. 方法作用
public native String intern()
主动将字符串放入字符串常量池,并返回池内引用
2. JDK1.6 与 JDK1.7 核心区别(面试必问)
JDK1.6(永久代常量池)
- 常量池有该字符串:返回池引用
- 常量池没有:复制一份放入永久代常量池,返回新池引用
- 堆对象和池对象完全独立
JDK1.7+(堆内常量池)
- 常量池有:直接返回池引用
- 常量池没有:不新建对象,直接把堆中对象引用存入常量池
- 实现堆对象 == 池对象,内存极致优化
3. 经典面试代码案例(JDK1.8)
String s = new String("1") + new String("1");
s.intern();
String s2 = "11";
System.out.println(s == s2); // true
解析
s是堆中11对象,常量池无s.intern():把堆s的引用存入常量池s2="11"直接复用池内堆引用- 两者指向同一堆对象 → true
4. intern() 使用场景
- 海量重复字符串,手动入池节省内存
- 统一字符串引用,
==判断生效 - 大数据、日志批量处理优化内存
五、高频面试考点汇总
- String 为什么不可变?final 数组+私有无修改方法
- 为什么推荐字符串拼接用 StringBuilder?效率最高无锁
- new String() 创建几个对象?1~2个
- 常量池位置变化:1.7从永久代移到堆
- 变量拼接和常量拼接区别:编译期优化
- intern() 1.6和1.7最大差异:是否复用堆引用
- String 不可变好处:安全、池化、哈希稳定
六、思维导图精简骨架
String体系
├─ 不可变性
│ ├─ 底层final char[]
│ ├─ 修改生成新对象
│ └─ 四大应用优势
├─ 三类字符串对比
│ ├─ String:不可变、低效
│ ├─ StringBuffer:线程安全、中等
│ └─ StringBuilder:单线程最快
├─ 字符串常量池
│ ├─ 内存位置变迁
│ ├─ 字面量 / new 两种创建
│ └─ 编译期拼接优化
└─ intern()方法
├─ 作用:入池
├─ JDK6 / JDK7 差异
└─ 实战内存优化
面试背诵短句版
一、面试极速背诵版(精简口诀+满分答案)
1. String不可变性
- 底层:
private final char[] value存储字符 - 不可变原因:final锁数组地址、private禁止外部修改、无修改API
- 修改本质:所有增删改查方法都新建字符串,原对象不变
- 四大好处:线程安全、常量池复用、hash值稳定、数据安全
- 误区:引用可变,内容不可变
2. String / StringBuilder / StringBuffer
- String:不可变,拼接低效,适合少量固定字符串
- StringBuffer:可变,加synchronized锁,线程安全,效率偏低
- StringBuilder:可变,无锁,单线程效率最高,线程不安全
- 选型口诀:常量用String、单拼用Builder、并发拼接用Buffer
3. 字符串常量池
- 位置:JDK1.6永久代,JDK1.7+移至堆内存
- 字面量创建:
String s="abc"→ 优先走常量池,最多1个对象 - new创建:
new String("abc")→ 常量池+堆,最少2个对象 - 编译优化:常量直接拼接编译期合并入池;变量拼接生成堆新对象
4. intern() 方法
- 作用:将字符串主动放入常量池,返回池中引用
- JDK1.6:无则复制新对象进永久代,堆与池不是同一对象
- JDK1.7+:无则直接存堆对象引用,堆和池指向同一个
- 用途:海量字符串去重、节省内存、统一引用判断
二、10道高频面试真题(含标准答案)
题1:String为什么不可变?
答:底层由private final char[]存储,final保证数组引用不可修改,private禁止外部访问修改,类中没有提供修改字符数组的方法,所以内容一旦创建无法更改。
题2:String拼接"+"底层原理是什么?
答:JDK8底层会自动创建StringBuilder进行append拼接,最后toString转String;循环内频繁+会频繁创建销毁Builder,性能极差。
题3:三者效率排序?
答:StringBuilder > StringBuffer > String
题4:String s = new String("xyz"); 创建几个对象?
答:常量池无xyz:2个(常量池1个+堆1个);常量池已有:1个(仅堆对象)。
题5:== 和 equals 区别?
答:==比较内存地址,equals默认比较地址,String重写后比较内容。
题6:字符串常量池存在意义?
答:复用字符串对象,减少内存占用,避免大量重复字符串创建,提升性能。
题7:String s1="a"+"b" 和 String s2=x+"b" 区别?
答:s1编译期优化为"ab",直接入常量池;s2是变量拼接,运行时创建堆中新对象,不入常量池。
题8:intern()方法JDK7之后变化?
答:池中没有该字符串时,不再创建新对象,直接把堆内对象引用存入常量池,实现堆对象与池对象同一地址。
题9:为什么String适合做HashMap的key?
答:不可变,hashCode值固定,不会变动导致找不到键,线程安全。
题10:循环拼接字符串用哪个?为什么?
答:用StringBuilder,可变字符数组,只扩容不新建对象,无线程锁,执行效率最高。
三、必背代码结论(JDK8环境)
// 1
String s1 = "abc";
String s2 = "abc";
s1 == s2 → true
// 2
String s3 = new String("abc");
s1 == s3 → false
// 3
String s = new String("1")+new String("1");
s.intern();
String s4 = "11";
s == s4 → true
Java String 全套【易混淆易错点对照表】
一、不可变性 易错区分
| 错误认知 | 正确结论 |
|---|---|
| String 引用不能变 | 引用可以变,内容不能变 |
| s+=拼接修改原字符串 | 每次拼接新建新对象,原对象不变 |
| substring 截取修改原串 | 全部返回新字符串 |
| final String 就不可改 | final只限制引用,内容依旧遵循不可变 |
一句话记:地址可改,字符内容永不可改。
二、String / StringBuilder / StringBuffer 最强区分
| 对比点 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变 | ❌ 不可变 | ✅ 可变 | ✅ 可变 |
| 线程安全 | 安全 | ✅ 安全(sync加锁) | ❌ 不安全 |
| 效率 | 最低 | 中等 | 最高 |
| 底层 | final char[] | 动态char[] | 动态char[] |
| 循环拼接 | 极慢 | 较慢 | 最快 |
| 推荐场景 | 常量、固定文本 | 多线程拼接 | 单线程所有拼接 |
死记口诀:
常量用String,并发用Buffer,日常全用Builder
三、创建字符串 对象数量 超级易错
String s = "abc"
- 池中有:0个新对象
- 池中无:1个常量池对象
String s = new String("abc")
- 池中有:只在堆创建1个
- 池中无:常量池1个 + 堆1个 = 2个
必考结论:new 最少1个,最多2个。
四、+拼接 编译期 & 运行期 分水岭
1)全常量拼接(编译期优化)
String s = "a"+"b"+"c";
编译直接变成 "abc",直接入常量池
2)含变量拼接(运行期)
String a = "a";
String s = a + "b";
底层new StringBuilder拼接,生成堆对象,不入常量池
判断口诀:全字面量=编译优化;有变量=堆新对象
五、== 与 equals 生死区别
- ==:比内存地址
- equals:String重写,比字符串内容
"abc" == new String("abc") → false
"abc".equals(new String("abc")) → true
业务开发一律用equals判相等
六、字符串常量池 位置变迁(面试必坑)
- JDK1.6 及以前:方法区(永久代)
- JDK1.7 ~ JDK17:移到堆 Heap
- 目的:永久代内存不足,方便GC回收闲置字符串
七、intern() 方法 JDK6 vs JDK7+ 最大坑点
JDK1.6
池内没有 → 新建一份字符串放进永久代
堆对象 ≠ 池对象,地址不同
JDK1.7+(主流)
池内没有 → 直接把堆对象引用存入常量池
堆对象 == 池对象,地址完全一致
经典必考代码结论(JDK8)
String s = new String("1") + new String("1");
s.intern();
String s2 = "11";
System.out.println(s == s2); // true
调换顺序直接变false
String s2 = "11";
String s = new String("1") + new String("1");
s.intern();
System.out.println(s == s2); // false
八、高频误区黑名单(背完不踩坑)
- 误区:String不可变就不会产生垃圾
正解:频繁拼接产生大量废弃字符串,极易造成内存浪费 - 误区:循环里直接用+拼接没事
正解:循环内+会疯狂创建销毁Builder,性能雪崩 - 误区:intern()一定会节省内存
正解:少量字符串没必要,海量重复字符串才适合用 - 误区:StringBuffer比StringBuilder快
正解:相反!Builder无锁更快 - 误区:常量池所有字符串都会被永久保存
正解:JDK7后在堆,闲置可被GC回收
九、最简答题万能模板
- 问不可变:private final char[] + 无修改方法
- 问拼接选谁:单线程StringBuilder,多线程StringBuffer
- 问new几个对象:字面量1个,new最多2个
- 问intern区别:6复制对象,7+存堆引用
- 问拼接原理:常量编译合并,变量底层Builder