Java 字符串驻留(intern())方法详解(Java 7+)
简单来说:
如果常量池中有该字符串就返回的是常量池中的对象引用,如果没有的话,返回这个堆对象本身的引用,同时常量池中存储堆对象的引用(指向堆对象)。
在 Java 7+ 中, intern() 的行为是:
如果池中没有等值字符串:
1.常量池中存储堆对象的引用(指向堆对象)
2.返回这个堆对象自身(相当于返回它本身的引用)
3.后续访问该字面值时,返回同一个堆对象引用
如果池中已有等值字符串:
1.不改变常量池的内容
2.返回池中原有的对象引用
3.堆对象不会被加入常量池
intern() 方法的核心行为(双模式)
intern() 方法的行为完全取决于调用时刻,字符串常量池中是否已存在与之 equals() 相等的字符串对象:
模式 1:常量池中不存在等值字符串时
String s1 = new String("A") + new String("B");
// 1. 在堆上创建新对象 "AB",池中尚无 "AB"
String s2 = s1.intern();
// 2. s1调用intern(): 池中无"AB",将s1的堆引用(指s1指向的堆对象)放入池,并返回此引用
String s3 = "AB";
// 3. 字面量创建:发现池中已有引用,直接返回该引用 (即s1的堆引用)
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s2 == s3); // true
结果:s1, s2, s3 全部引用同一个堆对象。intern() 将堆对象的引用添加到常量池。
模式 2:常量池中已存在等值字符串时
String preExisting = "AB";
// 0. 类加载时,"AB"字面量存入常量池 (假设)
String s1 = new String("AB");
// 1. new 在堆上创建全新对象 "AB",池中已存在 "AB"
// 注意: new String("AB") 的构造参数 "AB" 本身也在池中
String s2 = s1.intern();
// 2. s1调用intern(): 池中已有"AB",返回池中的对象引用
String s3 = "AB";
// 3. 字面量创建:返回池中的对象引用
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // false
System.out.println(s2 == s3); // true
结果:
s1 != s2(s1 是堆对象,s2 是池对象)s2 == s3(都为常量池对象)s1 != s3
行为对比表
| 条件 | intern() 行为 | 返回对象 | 常量池变化 |
|---|---|---|---|
| 常量池中不存在等值字符串 | 将当前堆对象引用添加到常量池 | 当前堆对象自身 | 新增一个引用映射 |
| 常量池中已存在等值字符串 | 不改变常量池,返回已有对象引用 | 常量池中的对象 | 无变化 |
关键理解点
字面量 vs 运行时创建
-
字面量(如
"AB"):- 类加载时加入常量池
-
new String("literal"):- 参数
"literal"仍是池中对象 - 总会在堆上新建对象,引用不同
- 参数
-
拼接(如
str1 + str2):- 运行时创建堆对象,除非编译期常量
首次出现原则
intern() 是否将堆对象引用放入池,仅取决于调用时刻池中是否存在等值对象:
实验 1:首次出现
String a = new String("Hel") + "lo";
// 运行时创建堆对象 "Hello"
a.intern();
// 首次出现!将a的引用存入池
String b = "Hello";
// 获取池中的引用 (即a的堆引用)
System.out.println(a == b); // true
实验 2:已存在
String pre = "Hello";
// 类加载时常量池已有 "Hello"
String c = new String("Hel") + "lo";
// 堆中创建新对象 "Hello"
c.intern();
// 返回池中已有引用 (忽略堆对象)
String d = "Hello";
// 返回池中原有引用
System.out.println(c == d); // false
System.out.println(pre == d); // true
最佳实践与注意事项
避免混用字面量和 new
// 不推荐:创建了多余堆对象
String s = new String("text");
// 推荐:复用池对象
String s = "text";
谨慎使用 intern()
-
适用场景:
- 内容重复大量字符串
- 能控制首次
intern()时机(如系统初始化)
-
性能风险:
intern()检查池需同步,存在开销- 大量使用可能导致堆膨胀,增加 GC 压力
- 池中已有重复值时重复检查造成浪费
-
替代方案:
- 使用
ConcurrentHashMap<String, String>+computeIfAbsent实现自定义驻留池 - 具备回收、清理等灵活性
- 使用
明确版本差异
| Java 版本 | intern() 行为 |
|---|---|
| Java 6 及之前 | 存储在 永久代(PermGen) 常量池大小固定,易 OOM |
| Java 7 及之后 | 存储在 堆 上的引用,常量池属堆的一部分 减少 OOM 风险,但仍可能导致堆压力上升 |
Java 7+ 的优化行为总结
在 Java 7+ 中,intern() 的行为如下:
当池中没有等值字符串时:
- 常量池中 存储堆对象的引用
- 返回该堆对象本身
- 后续访问字面量会 返回同一堆对象引用
当池中已有等值字符串时:
- 不改变常量池内容
- 返回池中已有对象的引用
- 堆对象 不会被加入常量池
结论
正如所指出的,intern() 的行为和 == 的比较结果完全取决于:
调用
intern()的那一刻,常量池中是否已有等值对象。
Java 7+ 中 intern() 优化为存储堆对象引用,这使我们更需深入理解常量池与堆之间的关系。 掌握这些规则,是精确理解和预测 intern() 行为的关键。