引言:String.intern() 被用来往字符串常量池中添加字符串,使得字符串的值可以复用,从而降低了内存开销。String.intern() 在JDK 7 中发生了重大变动,理解String.intern()改动后的原理能帮助我们正确使用它。
1 比较String两种创建方式
先来看下边一段代码
public class Main {
//第一段代码
/**
* @author 姚子威
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
String s= new String("1")+new String("1");
String s1= "11";
System.out.println(s==s1);
}
}
*运行的结果:false
![](https://i-blog.csdnimg.cn/blog_migrate/fc254203253f6ed3b4ac66fe36a79adc.png)
第一次通过两个字符串对象"1"的拼接,形成一个新的"11"字符串对象,这个"11"字符串对象是储存在堆中。JVM直接返回这个对象的引用给s变量。这时候字符串常量池中还没有"11"的引用,此时引用s指向堆中。
第二次通过字面量"11"创建,此时JVM会在字符串常量池中创建"11"字符串常量,并且返回新创建的"11"字符串的引用。引用s1指向字符串常量池。
有一点需要解释一下,上下文语境提到的堆指的是除了字符串常量池之外的堆内存空间。通过上文,可以知道s和s1指向的地址完全不同,所以结果返回的是false。
读者可能不解,在上述例图中字符串常量池有"1"字符串,我们将在下面的第三小节解释为什么会出现这种情况。
2 理解String.intern()工作原理
我们对上述代码做一点修改,加入String.intern(),看看会发生什么事情。
public class Main {
//第二段代码
/**
* @author 姚子威
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
String s= new String("1")+new String("1");
s.intern();
String s1= "11";
System.out.println(s==s1);
}
}
*运行的结果:true
![](https://i-blog.csdnimg.cn/blog_migrate/ddf51915ca6b7daab94e7d57a42d0390.png)
第二段代码和第一段代码唯一不同的地方就是加入了s.intern() ,这时候我们惊奇得发现返回的结果是true,这又是怎么一回事呢?
intern()方法的作用是先检查字符串常量池中是否有调用者(在这个例子中是引用s)指向的字符串,如果有,返回字符串常量池中这个字符串的引用,如果没有,说明此时字符串对象在堆中,那就在字符串常量池中记录该引用。
在第一次创建字符串对象并返回引用s后,s调用了intern()方法,在字符串常量池中记录下引用s的值。当第二次通过字面量"11"创建时,直接从字符串常量池中返回"11"这个字符串对象的引用,即引用s的值。这样就能够理解为什么第二段代码返回的值是true。
3 事情还没结束
理解了第一段代码和第二段代码,我们来做一个小题目,我们把第二段代码再修改一下。下面是第三段代码
public class Main {
//第三段代码
/**
* @author 姚子威
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
String s= new String("1");
s.intern();
String s1= "1";
System.out.println(s==s1);
}
}
请问得到的结果是什么?
我相信有一部分人会说 这跟第二段代码有什么区别?答案当然是true了。但是运行的结果是false!区别在哪里呢?
第二段代码第一次创建"11"是通过字符串的拼接得到的对象,本质上是通过StringBuilder.toString()得到并存放在堆中,所以此时字符串常量池中只有"1"字符串常量,没有"11"字符串常量。
而在第三段代码中,通过new String()方法 实际上做了两件事情,第一是创建了一个String实例在堆中,第二是创建了一个字符串常量在常量池中。这时候调用intern()方法,由于字符串常量池中已经有了"1"字符串,所以没有把堆中的实例的引用添加到字符串常量池中,此时s指向堆中的"1"实例,s1指向字符串常量池中的"1"字符串常量。
4 总结
(1) 通过字面量创建字符串,先检查常量池中是否有相同的字符串,如果没有,创建一个新的字符串实例并返回引用,如果有,直接返回该实例的引用。
(2) 通过构造器new String() 方法创建字符串,先在堆中创建实例并返回该引用,然后检查常量池中是否有相同的字符串常量,如果没有,在字符串常量池中创建一个相同的字符串常量,如果有,则仅在堆中创建Java层面的String实例。
(3)通过字符串拼接的而成的字符串实例不会出现在字符串常量池中,只会在堆中出现(如果是单纯字面量拼接则直接指向字符串常量池)。
(4)在JDK 7 以后String.intern()方法不会再复制字符串到常量池中,只会检查常量池中是否有相同的字符串,如果有,则返回该字符串的引用。如果没有,说明此时字符串对象在堆中,这时候只需记录下堆中对象的引用到常量池中,并且返回该引用。
5 结语
(1)对于字符串常量池及其相关知识点还是比较繁杂,博主也是基于已有认知进行总结,如果有错误的地方,希望大家能在评论区指出。
(2)本文的代码样例是参考美团技术团队的文章给出的。
6 参考
(1)《深入理解Java虚拟机》-周志明
(2) 美团技术团队文章:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html