String的#intern()方法新收获

引入

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

题外话之运行时常量池、字符串常量池的关系及所在位置

  • 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代。

  • 在JDK1.7字符串常量池和静态变量被从方法区拿到了堆中,运行时常量池剩下的还在方法区, 也就是hotspot中的永久代。

  • 在JDK8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)。方法区的实现,用的是本地内存(Native Memory)。

总结:目前 (jdk8及之后),字符串常量池不包含在运行时常量池中 (曾经逻辑包含),字符串常量池在堆中,运行时常量池在方法区中。

延伸

public static void main(String[] args) {
        String s = new String("aa");
        String s1 = s.intern();
        String s2 = "aa";

        System.out.println(s == s2);  //①
        System.out.println(s1 == s2); //②

        String s3 = new String("b") + new String("b");
        String s4 = s3.intern();
        String s5 = "bb";

        System.out.println(s3 == s5 ); //③
        System.out.println(s4 == s5);  //④
}

上面这段代码的输出是?

第一反应

false
true
false    
true

①输出false,因为s是堆中对象的引用,s2是字符串常量池中对象的引用;②输出true,因为s1和s2是字符串常量池中同一对象的引用;③④跟①②相比,也没什么差别嘛。③输出false,因为s3是堆中对象的引用,s5是字符串常量池中对象的引用;④输出true,因为s4和s5是字符串常量池中同一对象的引用。(ps:s4和s5不是字符串常量池中对象的引用,后面会解释)

真是如此吗?

在jdk8下,程序运行后,结果出乎我的意料,输出结果竟然是

false
true
true    // 出乎我意料的输出
true

String的#intern()方法新收获

 

 看下String源码中#intern()方法的一段注释:

* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.

字面意思就是,当调用这个方法时,会去检查字符串常量池中是否已经存在这个字符串,若存在,就直接返回;若不存在,就把这个字符串常量加入到字符串常量池中,然后再返回其(已有)引用

但是,其实在JDK1.6和 JDK1.7的处理方式是有一些不同的。

在JDK1.6中,如果字符串常量池中已经存在该字符串对象,则直接返回池中此字符串对象的引用。否则,将此字符串的对象添加到字符串常量池中,然后返回该字符串对象的引用 (备注:返回的是字符串常量池中对象的引用)。

在JDK1.7中,如果字符串常量池中已经存在该字符串对象,则返回池中此字符串对象的引用。否则,如果堆中已经有这个字符串对象了,则把此字符串对象的引用添加到字符串常量池中并返回该引用 (返回的是堆中对象的引用!);如果堆中没有此字符串对象,则先在堆中创建字符串对象,再返回其引用。(这也说明,此时字符串常量池中存储的是对象的引用,而对象本身存储于堆中

于是代码中,String s = new String("aa");创建了两个“aa”对象,一个存在字符串常量池中,一个存在堆中。

String s1 = s.intern(); 由于字符串常量池中已经存在“aa”对象,于是直接返回其引用,故s1指向字符串常量池中的对象。

String s2 = "aa"; 此时字符串常量池中已经存在“aa”对象,所以也直接返回,故 s2和 s1的地址相同。②返回true。

System.out.println(s == s2); 由于s的引用指向的是堆中的“aa”对象,s2指向的是常量池中的对象。故不相等,①返回false。

重点来了

String s3 = new String("b") + new String("b"); 先说明一下,这种形式的字符串拼接,等同于使用StringBuilder的append方法把两个“b”拼接,然后调用toString方法,new出“bb”对象,因此“bb”对象是在堆中生成的。所以,这段代码最终生成了两个对象,一个是“b”对象存在于字符串常量池中,一个是 “bb”对象,存在于堆中,但是此时字符串常量池中是没有“bb”对象的。s3指向的是堆中的“bb”对象。

String s4 = s3.intern(); 调用了intern方法之后,在JDK1.6中,由于字符串常量池中没有“bb”对象,故创建一个“bb”对象,然后返回其引用。所以 s4 这个引用指向的是字符串常量池中新创建的“bb”对象。在JDK1.7中,则把堆中“bb”对象的引用添加到字符串常量池中,故s4和s3所指向的对象是同一个,都指向堆中的“bb”对象。

String s5 = "bb"; 在JDK1.6中,指向字符串常量池中的“bb”对象的引用,在JDK1.7中指向的是堆中“bb”对象的引用。

System.out.println(s3 == s5 ); 参照以上分析即可知道,在JDK1.6中③返回false(因为s3指向的是堆中的“bb”对象,s5指向的是字符串常量池中的“bb”对象),在JDK1.7中,③返回true(因为s3和s5指向的都是堆中的“bb”对象)。

System.out.println(s4 == s5); 在JDK1.6中,s4和s5指向的都是字符串常量池中创建的“bb”对象,在JDK1.7中,s4和s5指向的都是堆中的“bb”对象。故无论JDK版本如何,④都返回true。

综上,在JDK1.6中,返回的结果为:

false
true
false    
true

在JDK1.7中,返回结果为:

false
true
true    
true

因此,在jdk8下,我上面【第一反应】的理解是错的,s4和s5不是字符串常量池中对象的引用,而是堆中对象的引用。

参考链接

CS-Notes

#intern()延伸参考文章

运行时常量池、字符串常量池的关系及所在位置参考文章

上一篇:StringBuilder-比-String-快?空嘴白牙的,证据呢,java面试问项目的难点


下一篇:String 既然能做性能调优,我直呼内行