测试前提:
JDK1.8环境下!
我们先来看一下String中的intern方法:
public native String intern();
要想了解该方法,我们先要了解String的创建规则。
一、创建字符串分析
1、双引号创建字符串
判断这个常量是否存在于常量池
如果存在
判断这个常量存的是引用还是常量
如果是引用,返回引用地址指向的堆空间对象
如果是常量,则直接返回常量池常量
如果不存在
在常量池中创建该常量,并返回此常量
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);// true
2、new String创建字符串
在堆中创建对象(无论堆中是否存在相同字面量的对象)
然后判断常量池上是否存在字符串的字面量
如果不存在
在常量池中创建字面量的常量
如果存在
不做任何操作
String a1 = new String("AA");
String a2 = new String("AA");
System.out.println(a1 == a2);// false
3.双引号相加
判断这两个常量、相加后的常量在常量池上是否存在
如果不存在
则在常量池上创建相应的常量
如果存在
判断这个常量是存在的引用还是常量,
如果是引用,返回引用地址指向的堆空间对象
如果是常量,则直接返回常量池常量
String s1 = "AA" + "BB";
String s2 = "AABB";
String s3 = "AA" + "BB";
System.out.println(s2 == s3);// true
System.out.println(s1 == s2);// true
System.out.println(s1 == s3);// true
4.两个new String相加
首先会创建这两个对象以及相加后的对象
然后判断常量池中是否存在这两个对象的字面量常量
如果存在
不做任何操作
如果不存在
则在常量池上创建对应常量
// 只在堆上创建AABB对象,没有在常量池中创建常量AABB
String s1 = new String("AA") + new String("BB");
String s2 = "AABB";
System.out.println(s1 == s2);
5.双引号字符串与new String字符串
首先创建两个对象,一个是new String的对象,一个是相加后的对象
然后判断双引号常量与new String的字面量在常量池是否存在
如果存在
不做操作
如果不存在
则在常量池上创建对象的常量
(本质就是调用stringBuilder.append()在堆上创建新的对象)
// 创建两个对象 "BB"的对象和"AABB"的对象 然后在常量池中创建"AA","BB"的常量
String s2 = "AA" + new String("BB");
String s1 = "AABB";
System.out.println(s1 == s2); // false
二、String.intern()分析
判断这个常量是否存在于常量池。
如果存在
判断存在内容是引用还是常量,
如果是引用,
返回引用地址指向堆空间对象,
如果是常量,
直接返回常量池常量
如果不存在,
将当前对象引用复制到常量池,并且返回的是当前对象的引用
我们直接对一个案例进行分析,帮助大家快速了解这个方法:
String a3 = new String("AA");
a3.intern();
String a4 = "AA";
System.out.println(a3 == a4); // false
如果有不懂的小伙伴,看完这个案例输出,嘴里可能已经冒出why?了
那我们来进行逐句分析:
String a3 = new String("AA"); (在堆中创建了一个对象,并且在常量池中创建了该常量) a3.intern(); (这里对不懂的小伙伴其实是有点坑的,这个方法执行时,常量池当中时有"AA"的常量的,所以该方法的返回值是常量。) String a4 = "AA"; (常量池中有常量,所以返回的是常量) System.out.println(a3 == a4); // false
(此时a3引用的是堆中的对象,a4则是常量池中的常量,所以返回false)
那我们更改一下语句:
String a3 = new String("AA"); String a4 = "AA"; System.out.println(a3.intern() == a4); // true
(此时,我们是用a3.intern()方法的返回值和a4进行比较,a3.intern()方法的返回值是常量,a4值向的也是常量池中的常量,所以返回true)
总结:上面两个示例,我们需要搞清楚的就是a3 是一个堆中的对象,而a3.intern()是一个方法,返回的值按照该方法的规则来,本示例中,返回的是常量池中的常量。
看到这你就以为结束了?对String.intern方法很了解了?NoNoNo!
下面让我们来迎接更难的题目!
String s2 = "AA" + new String("BB");
s2.intern();
String s1 = "AABB";
System.out.println(s1 == s2); // true
String s2 = "AA" + new String("BB"); (创建"BB" 和 "AABB"对象,在常量池中创建"AA","BB"常量) s2.intern(); (此时常量池中没有"AABB",将当前对象引用复制到常量池,并且返回的是当前对象的引用) String s1 = "AABB"; (此时常量池中存在引用,返回引用地址指向的堆空间对象) System.out.println(s1 == s2); // true
我们将2,3句调换位置
String s2 = "AA" + new String("BB"); String s1 = "AABB"; (此时常量池中还没有"AABB",在常量池中创建常量"AABB") s2.intern(); (常量池中存在常量,则直接返回常量池常量) System.out.println(s1 == s2); // false
(s2为堆内存中对象,s1为常量池中常量,所以为false)
哎,还没完哟,别着急划走,我们再看一道题!,我保证,这是最后亿道!
String s3 = new String("A") + new String("A");
String s4 = "AA";
s3.intern();
System.out.println(s3 == s4); // false
String s3 = new String("A") + new String("A"); (创建两个对象和相加后的对象,在常量池中创建常量"A") String s4 = "AA"; (常量池中没有"AA",创建该常量) s3.intern(); (常量池中存在该常量,则直接返回该常量) System.out.println(s3 == s4); // false
我们再将2,3句进行调换
String s3 = new String("A") + new String("A"); s3.intern(); (常量池中没有数据,则将当前对象引用复制到常量池,并且返回的是当前对象的引用) String s4 = "AA"; (此时常量池中有引用,则直接返回引用地址) System.out.println(s3 == s4); // true
三、拓展(jdk1.6及jdk1.7之后intern方法的不同)
我们要知道,以下代码在jdk1.6及jdk1.7之后执行结果是不同的
String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); // false(1.6) true(1.7后)
原因下文中会进行解答。
我们先了解jdk1.6 和 jdk1.7中intern()方法的区别
1、JDK1.6
因为JDK1.6以及以前版本中,常量池是放在 Perm 区(属于方法区)中的,熟悉JVM的话应该知道这是和堆区完全分开的。
在JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代中这个这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果为false。
2、JDK1.7
而JDK1.7之后的intern方法实现就不需要再拷贝字符串的实例道永久代中了,既然字符串常量池已将移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可。因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。
参考书籍:《深入理解Java虚拟机》第三版--63页