Yves

String#intern()

String 我们经常使用,但是 String String#intern() 这个方法却似乎很少见。
在《深入理解 Java 虚拟机》运行时常量池一节中提到:

并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

那么这个 intern() 方法有什么用呢?

JavaDoc 的解释是,当调用 intern() 方法时,首先检查常量池中是否存在一个与这个字符串对象相等(equals)的字符串,如果存在,则返回常量池中的字符串的引用,如果不存在,则把当前字符串的引用添加进常量池,然后返回这个引用。
这就可以保证,返回值是来自常量池的引用,而且常量池中的字符串是唯一的。
另外所有文字字符串和字符串值常量表达式,也就是像 "1"; String s = "a" + "b"; 这样的字符串(一共几个呢?),都是在常量池中的。
这也引申出一个常见的问题 —— String s = new String("aString");创建了几个对象。答案是两个,一个是放在常量池中的字符串”aString”,还有一个是 new 出来的 String 对象,其值等于 “aString”,但是地址肯定不一样,因为 new 指令创建的对象放在 Java Heap 中。(对于 HotSpot VM 来说常量池也是位于 Java Heap 中)

Talk is cheap,show me the code

1
2
3
4
String s1 = new String("1") + new String("1");
String s2 = "11";
System.out.println(s1 == s2);
System.out.println(s1.intern() == s2);

运行结果:

1
2
false
true

对于引用 s1 而言,指向的对象是位于 Java Heap 中的对象,s2 则指向常量池中的对象。而 s1.intern() 方法首先检查常量池中是否存在字符串 “11”,结果当然是存在了,String s2 = "11";已经将字符串 “11” 放进去了。

那么现在该一下执行顺序,先执行 intern() 会怎么样?在第二行插入一句 s1.intern()

1
2
3
4
5
String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);
System.out.println(s1.intern() == s2);

运行结果:

1
2
true
true

可以看到 s1 == s2 的值为 true 了!我们并没有改变 s1 的引用,仍然指向 Java Heap 中的对象,但是当执行 s1.intern() 的时候,检查到常量池中还没有值为 “11” 的字符串,所以将其引用,添加到常量池中,所以整个内存中还是只有一个 “11” 字符串对象。这时候执行 String s2 = "11";,”11”是显示声明的,所以去常量池中去创建,但是发现这时常量池中已经有指向 “11” 字符串对象的引用了,直接返回该引用,所以 s2 最终也指向了这个对象,所以 s1 和 s2 引用的对象是同一个对象,也就是 s1 == s2 的值为 true。

在 JDK 1.6 之前,执行 s1.intern() 的时候是直接将 Java Heap 中的 “11” 字符串对象拷贝到常量池中,所以 s1 == s2 将会是 false!因为 JDK 1.6 的常量池放在 Perm 区,与 Heap 区域是完全分离的,而 JDK 1.7 之后常量池是作为 Heap 的一个逻辑部分。所以在两个版本间运行结果会有些差异

再通过一个例子说明其优点

1
2
3
4
String[] arr = new String[10000];
for (int i = 0; i < arr.length; i++) {
arr[i] = String.valueOf(i % 100);
}

上面的代码中,会在 Java Heap 中产生 10000 个 String 对象,而如果第 3 行改为:

1
arr[i] = String.valueOf(i % 100).intern();

则最终只会保留 100 个 Java Heap 中的对象,因为当 i == 100 的时候,i % 100 == 0,而 “0” 在 Java Heap 中已经存在了,intern() 方法会在常量池中创建一个引用指向 “0”,并返回该引用,那么 String.valueOf() 方法创建的对象就会在下一次 gc 的时候回收掉。