String.intern() 是 JDK 6 开始有的方法,面试经常被问到,但很多人只停留在"字符串驻留"这个概念上,具体怎么回事不太清楚。
一、intern 的基本行为
调用 intern() 后,如果字符串池里已经有一个等于此对象的字符串,直接返回池里的那个;如果没有,把此对象加入字符串池,返回池里的引用。
java
String s1 = new String("abc");
String s2 = s1.intern();
System.out.println(s1 == s2); // false:s1 是堆对象,s2 是池里的
但 intern 用不好容易踩坑。
二、一个经典面试题
java
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
String s3 = new String("ab");
System.out.println(s1 == s2); // true 还是 false?
先说答案:JDK 6 是 false,JDK 7+ 是 true。
解释一下执行过程:
JDK 6:
s1 = new String("a") + new String("b")在堆里创建两个对象 "a" "b",拼接后生成 "ab",s1 指向堆s2 = s1.intern()把 "ab" 复制一份到字符串池,返回池地址给 s2- s1 在堆,s2 在池,所以
s1 == s2是 false
JDK 7+:
s1.intern()不再复制字符串到池,只是记录一下引用- 字符串池搬到了堆里,"ab" 本身就是从堆里产生的,直接记录引用就行
- 所以 s1 和 s2 指向同一个对象,
s1 == s2是 true
三、intern 能省内存
适用场景:大量重复的字符串,只存一份能省不少内存。
java
// 假设有 100 万个 userName,但实际取值只有几千种
String[] names = new String[1000000];
for (int i = 0; i < 1000000; i++) {
names[i] = new String(getUserName()).intern();
}
intern 后,所有相同的字符串都指向字符串池里的同一个对象,内存占用会降下来。
但要注意:intern 会把字符串放入字符串池,如果取值种类太多(比如 UUID),反而浪费字符串池的内存。
四、常见用法:字符串比较
用 intern 做字符串字面量比较:
java
// 不用 intern:每次比较都要遍历字符数组
if (str.equals("POST")) { }
// 用 intern:直接比较引用
if (str.intern() == "POST") { }
在高频比较场景下,引用比较比 equals 更快。
五、JDK 8 的陷阱
JDK 8 里字符串拼接和 intern 的交互比较绕:
java
String a = "ab";
String b = "cd";
String c = "abcd";
String d = a + b; // 编译期不会优化,结果是 new StringBuilder().append(a).append(b).toString()
String e = "ab" + "cd"; // 编译期直接优化成 "abcd"
System.out.println(c == d); // false:c 是池,"abcd",d 是堆 new 出来的
System.out.println(c == e); // true:e 在编译期就优化成 "abcd",也是池里的
所以 "ab" + "cd" 和 a + b 看起来一样,但执行时机完全不同。
六、什么场景适合用 intern
| 场景 | 用 intern | 原因 |
|---|---|---|
| 大量重复的字符串 | √ | 节省内存 |
| 高频字符串比较 | √ | 引用比较比 equals 快 |
| 取值种类极多的字符串 | X | 浪费字符串池空间 |
| 动态生成的字符串 | X | intern 本身也有开销 |
总结
- intern 返回字符串池里的引用
- JDK 6 会复制字符串到池,JDK 7+ 只是记录引用
- 用好了能省内存、做快速比较
- 用不好(取值种类太多)反而浪费内存
大多数业务代码里其实不太需要自己调用 intern,但如果面试问到,知道原理比背答案有用。