理解Java中String.intern()方法

java string.intern

Posted by alovn on April 7, 2019

String.intern() 的作用

String.intern() 方法可以使得所有含相同内容的字符串都共享同一个内存对象, 它可以减少内存中相同字符串的数量,节省一些内存空间。

JVM 中,存在一个字符串常量池,字符串的值都存放在这个池中。当调用 intern 方法时,如果字符串常量池中已经存在该字符串,那么返回池中的字符串;否则将此字符串添加到字符串常量池中,并返回字符串的引用。

不同jdk版本下的区别

JDK1.6 和 JDK1.7 在 intern() 方法的实现上,有相同,也有不同。

相同点: 先去查看字符串常量池是否有该字符串,如果有,则返回字符串常量池中的引用。 不同点: 如果是 JDK1.7,当字符串常量池中找不到对应的字符串时,不会将字符串拷贝到字符串常量池,而只是生成一个对该字符串的引用在字符串常量池。而 JDK1.6 会拷贝字符串至字符串常量池。

在jdk1.6 以及之前,字符串常量池是存在于永久代中的(方法区),在jdk 1.7  常量池移动到了堆空间中,并且常量池中只存储字符串的引用,不再存储字符串值,在jdk1.8 又进行改变,字符串常量池已经不存在,又出现了一个metaSpace(元空间) 。

注意:字符串常量池中的 String 对象,也是可以被 GC 回收的,只要它不再被引用了。

Java源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* 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.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cite>.
*
* @return  a string that has the same contents as this string, but is
*          guaranteed to be from a pool of unique strings.
*/
public native String intern();

这是一个本地方法,jdk源码中没有实现,它是通过 jni 调用c++实现的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是1009。要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:

1
-XX:StringTableSize=99999

注释大致翻译:

1
2
3
4
5
返回一个规范的字符串表现形式。
字符串池初始是空的,由String维护。
当调用intern(),如果在字符串池中含有(该字符串equals后的结果值),则返回字符串池中的结果值。(有点绕,意思就是有相同字符集的,就返回该字符集);
如果没有,则将该字符集加入常量池中,再调用intern()。
所以,任何的String类型比较其intern()时,都是比较equals值。

字符串创建的方式

如果想了解字符串的intern操作产生的影响,必须首先知道字符串对象的创建方式,一般字符串有四种创建方式,如下

1
2
3
4
5
6
7
8
9
10
//1
String str1 = "hello";  // 字面两直接赋值
//2
String str2 = new String("hello"); // 调用字符串构造方法生成对象
//3
String str3 = str1 + str2;
//4
final String str4 = "hello";
final String str5 = "world";
String  str6 = str4 + str5;

第一种,如果使用字符串字面量进行字符串对象的创建,那么会首先去常量池中查看是否已经存在了该值,如果存在的话,直接返回该对象的引用。

第二种, 该方法对直接在堆上分配内存空间创建该对象。

第三种, jvm 会在编译的时候将其优化为StringBuilder 的拼接,其也生成了新对象 StringBuilder 注意该对象是线程不安全的。

第四种,由于是字符串常量拼接,故jvm会在编译期将其替换为为常量,就相当于第一种方式,先去常量池查看。

代码示例

可配合以下代码示例进行理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringIntern {
    public static void main(String[] args) {
        String s1 = "HelloWorld";
        String s2 = new String("HelloWorld");
        String s3 = "Hello";
        String s4 = "World";
        String s5 = "Hello" + "World";
        String s6 = s3 + s4;
 
        System.out.println(s1 == s2);  //false
        System.out.println(s1 == s5);  //true
        System.out.println(s1 == s6);  //false 此处s6=new String(s3 + s4)
        System.out.println(s1 == s6.intern());//true
        System.out.println(s2 == s2.intern());//false
    }
}

StringBuilder

StringBuilder的情形和String不太一样,StringBuilder并不对常量池进行操作。