前言
最近遇到一个Intern()方法,代码如下,在 jdk1.8 的环境下得到如下的测试结果,给我整不会了,因此研究了一下这个方法,记录一下:
1 package com.example.demo.test; 2 3 /** 4 * @description: 5 * @author: luguilin 6 * @date: 2022-02-25 11:14 7 **/ 8 public class TestString { 9 static void test01(){ 10 String s1 = new String("1")+new String("23"); 11 s1.intern(); 12 String s2 = "123"; 13 System.out.println( s1 == s2);//true 14 } 15 16 static void test02(){ 17 String s1 = new String("1")+new String("23"); 18 String s2 = "123"; 19 s1.intern(); 20 System.out.println( s1 == s2); //false 21 } 22 23 static void test03(){ 24 String s1 = new String("1")+new String("23"); 25 String s2 = "123"; 26 System.out.println( s1 == s2);//false 27 s1.intern(); 28 System.out.println( s1 == s2);//false 29 s1 = s1.intern(); 30 System.out.println( s1 == s2);//true 31 } 32 33 public static void main(String[] args) { 34 test01(); 35 System.out.println("-----------------"); 36 test02(); 37 System.out.println("-----------------"); 38 test03(); 39 } 40 }
不说别的,上述方法中的test01(),为什么是True?
在我之前的印象里,s2指向方法区中的常量,s1应该指向的是堆上的对象,二者应该是不一样的啊?为什么是对的?
而我调换了一下s1.intern()语句的顺序以后,就又是false了。当我s1=s1.intern()以后,又相等了。这下彻底给我搞蒙了。
1、java的Intern()方法
在 jdk1.8 中,intern方法的定义 在Java的String类中是这样定义的,是一个本地方法,其中源码由C实现
public native String intern();
再来看一下源码的注释描述:
* <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>
(直译:当调用 intern 方法时,如果池中已经包含一个与该方法确定的对象相等的字符串,则返回池中的字符串。 否则,将此对象添加到池中并返回对该对象的引用。)
翻译过来的意思就是:如果常量池中已经有了此字符串,那么将常量池中该字符串的引用返回,如果没有,那么将该字符串对象添加到常量池中,并且将引用返回。
首先要明白,这里注释的该字符串是调用此方法的字符串,返回的是引用。
代码如下:在 jdk1.8 中运行
1 package com.example.demo.test02; 2 3 4 public class TestIntern { 5 void test01(){ 6 String s1 = new String("xyz"); 7 String s2 = "xyz"; 8 System.out.println(s1==s2); // false 9 } 10 11 void test02(){ 12 String s2 = "xyz"; 13 String s1 = new String("xyz"); 14 System.out.println(s1==s2); // false 15 } 16 17 public static void main(String[] args) { 18 TestIntern ins = new TestIntern(); 19 ins.test01(); 20 ins.test02(); 21 } 22 }
2、new String("xyz")会创建几个对象?
动手实践后,发现再new String("xyz")有可能会创建一个(不好验证,但是可以通过下文的分析得出结论),也有可能会创建两个(可验证)。
结论:如果常量池中没有 xyz,那么就会创建两个,现在堆中创建一个,然后将对象copy到常量池中,也就是第二次创建,堆中和常量池中是两个对象。
事实上,在不同的jdk版本中,intern()方法的实现是不一样的,主要原因是永久代的去除和元空间的增加,见《java 内存分布》和《聊聊JVM分代模型:年轻代、老年代、永久代》。
Java6版本:
intern方法作用:确实如上述注释上所描述,如果常量池中没有字符串,则将该字符串对象加入常量池,并返回引用。
** 这里需要注意:Java6中常量池是在方法区中,而Java1.6版本hotspot采用永久带实现了方法区,永久代是和Java堆区分的,即就是常量池中没有字符串,那么将该字符串对象放入永久代的常量池中,并返回其引用。
Java7和Java8版本:
intern方法作用:和注释描述的并不同,
如果常量池有,那么返回该字符串的引用。
如果常量池没有,那么如果是"a".intern调用,那么就会把"a"放入常量池,并返回"a"在常量池中的引用。
如果是new String("a").internal ,其中在 new String的时候上文已经说到过,会在堆和常量池各创建一个对象,那么这里返回的就是常量池的字符串a的引用。
如果是new StringBuilder("a").internal,其中new StringBuilder会在堆中创建一个对象,常量池没有,这里调用intern方法后,**会将堆中字串a的引用放到常量池,注意这里始终只是创建了一个对象,
返回的引用虽然是常量池的,但是常量池的引用是指向堆中字串a的引用的。
再简单总结一下Java7和Java8的intern方法作用:
如果常量池没有,那么会将堆中的字符串的引用放到常量池,注意是引用,然后返回该引用。为什么Java7和Java8会不一样呢,原因就是 Java7之后(部分虚拟机,Hotspot,JRockit)已经将永久代的常量池、静态变量移出,放入了Java堆中,而永久代也在Java8中完全废弃,方法区改名为元空间。
既然常量池已经在Java6之后放入了堆中,那么如果堆中已经创建过此字符串的对象了,那么就没有必要在常量池中再创建一个一毛一样的对象了,直接将其引用拷贝返回就好了,因为都是处于同一个区域Java堆中。
3、实践验证
3.1 实际验证一下上述结论
1 package com.example.demo.test02; 2 3 4 public class TestIntern { 5 6 /** 7 * 在new的时候已经创建了两个对象,第二行,只是获取的第一行创建的常量池的对象的引用,实际的对象已经创建过了。 8 * 这里是两个不同的对象,返回false。 9 */ 10 void test01() { 11 String s1 = new String("xyz"); 12 String s2 = "xyz"; 13 System.out.println(s1 == s2); // false 14 } 15 16 /** 17 * 和上述一样,只不过这一次第一行,现在常量池创建了对象,第二行发现常量池已经有了,只在堆上创建了一次对象. 18 * 但仍然是两个对象,引用不同,返回false。 19 */ 20 void test02() { 21 String s2 = "xyz"; 22 String s1 = new String("xyz"); 23 System.out.println(s1 == s2); // false 24 } 25 26 /** 27 * 第一行,StringBuilder只会在堆中创建一个对象,第二行调用intern方法后,会将堆中的引用放到到常量池中。 28 * 第三行发现常量池中已经有这个字符串的引用了,直接返回。 29 * 因此是同一个引用,返回的都是第一次创建的堆中字串的引用 30 */ 31 void test03() { 32 StringBuilder s1 = new StringBuilder("xyz"); 33 String s2 = s1.toString().intern(); 34 String s3 = "xyz"; 35 System.out.println(s2 == s3); // true 36 } 37 38 /** 39 * 和上述3的不同之处在于没有调用intern方法,因此结果输出不一样。 40 */ 41 void test04() { 42 StringBuilder s1 = new StringBuilder("xyz"); 43 String s2 = s1.toString(); 44 String s3 = "xyz"; 45 System.out.println(s2 == s3); // false 46 } 47 48 /** 49 * new String之后使用 + 在Java中会进行编译优化,编译成字节码指令后,会将 + 优化成 先new Stringbuilder对象,然后调用append方法进行拼接。 50 * 因此这里s1最终创建的时候,xyzz字符串并没有在常量池创建,只是在堆中创建了,因为就如同上面的test03()一样,是new Stringbuilder操作。 51 * 所以在调用intern操作后,将其堆中的引用放入常量池并返回。 52 * 所以后面的结果都是true,因为至始至终都是堆中的一个对象。 53 */ 54 void test05() { 55 String s1 = new String("xyz") + new String("z"); 56 String s2 = s1.intern(); 57 String s3 = "xyzz"; 58 System.out.println(s1 == s2); // true 59 System.out.println(s1 == s3); // true 60 System.out.println(s2 == s3); // true 61 } 62 63 /** 64 * 和上述test05()是相反的,结果输出也不同。 65 */ 66 void test06() { 67 String s1 = new String("xyz") + new String("z"); 68 String s3 = "xyzz"; 69 System.out.println(s1 == s3); // false 70 } 71 72 /** 73 * s1指向的对象并没有改变 74 * s2指向常量区,s1指向堆,所以不一样 75 */ 76 void test07() { 77 String s1 = new String("xyz") + new String("z"); 78 String s2 = "xyzz"; 79 s1.intern(); 80 System.out.println(s1 == s2); // false 81 } 82 83 /** 84 * s1.intern()之后,在常量区添加了堆中"xyzz"的引用,s2指向了这个常量池中"xyzz"对象 85 * 因此二者不相等 86 */ 87 void test08() { 88 String s1 = new String("xyz") + new String("z"); 89 s1.intern(); 90 String s2 = "xyzz"; 91 System.out.println(s1 == s2); // false 92 } 93 94 /** 95 * 第一个判断, 96 * s1.intern()之后,在常量区添加了堆中"xyzz"的引用 97 * s2也指向了常量池中这个引用,但是s1本身没有变,指的是堆中对象的引用,因此不相等 98 * <p> 99 * 第二个判断, 100 * s1 = s1.intern()以后,s1也指向了常量池中这个引用,因此相等 101 */ 102 void test09() { 103 String s1 = new String("xyz") + new String("z"); 104 s1.intern(); 105 String s2 = "xyzz"; 106 System.out.println(s1 == s2); // false 107 s1 = s1.intern(); 108 System.out.println(s1 == s2); // true 109 } 110 111 112 public static void main(String[] args) { 113 TestIntern ins = new TestIntern(); 114 ins.test01(); 115 ins.test02(); 116 ins.test03(); 117 ins.test04(); 118 ins.test05(); 119 ins.test06(); 120 ins.test07(); 121 ins.test08(); 122 ins.test09(); 123 } 124 }
3.2 另一个面试题
1 public class Test { 2 public static void main(String[] args) { 3 String str1 = new StringBuilder("计算机").append("软件").toString(); 4 String str2 = str1.intern(); 5 String str3 = new StringBuilder("ja").append("va").toString(); 6 String str4 = str3.intern(); 7 System.out.println(str1==str2); 8 System.out.println(str3==str4); 9 } 10 }
jdk1.8的输出答案是true和false。
jdk1.6的输出是两个false。
其他都好理解,为什么在1.8中,java这个变量,第二个判断,是false呢?这两代码不是一样么?理论上不应该是true么?
分析
在jdk1.6中
intern方法会把首次遇到的字符串复制到方法区中,返回的也是方法区这个字符串的引用。
而由StringBuilder创建的字符串实例在Java堆上,所以必然不是一个引用,所以返回false
str1指向堆,str2指向方法区,所以返回结果返回false
同理,str3和str4的返回结果也为false
下来我们看一看jdk1.7的intern方法
jdk1.7的intern方法不会在复制实例,只是在常量池中记录首次出现的实例引用。
因此str2指向的引用其实就是str1指向Java堆中StringBuilder创建的字符串实例。所以返回结果为true
但是java这个字符串常量在编译期就已经在方法区的常量池中了,不符合首次出现,所以str4指向的是常量池中的java字面量
所以返回结果为false。
问题又来了,java这个字面量为什么在编译期就出现在了常量池。我们可以进入System类中。看看有什么东西。
进入System类之后,我们发现这里有一个Version.init方法
再次进去查看
再次进去查看
哇,这么多常量,包括java,版本号,此版本号,都已经加载到常量池中,所以当我们调用str3.intern()方法时,java字面量已经存在,不符合首次出现,所以返回false,同理,我们也可以试一试这里的字面量,发现返回都是false。
String类的一个intern方法,涉及到了Java堆,java运行时常量池,涉及面很广泛,如果你不了解,是不是很吃亏。
4、最开始的问题
看了这么多以后,我以为我搞明白了最开始的问题,我突然发现,我最开始的代码和是第三节中代码是不一样的,如下,每一个代码都是静态方法:
下面一些栗子,自己思考吧,我也糊涂了
package com.example.demo.test; /** * @description: * @author: luguilin * @date: 2022-02-25 11:14 **/ public class TestString { static void test01(){ String s1 = new String("1")+new String("23"); String s2 = "123"; s1.intern(); System.out.println( s1 == s2); //false } static void test02(){ String s1 = new String("1")+new String("23"); s1.intern(); String s2 = "123"; System.out.println( s1 == s2);// false } void test03(){ String s1 = new String("1")+new String("23"); s1.intern(); String s2 = "123"; System.out.println( s1 == s2);// false } static void test04(){ String s1 = new String("1")+new String("23"); String s2 = "123"; System.out.println( s1 == s2);//false s1.intern(); System.out.println( s1 == s2);//false s1 = s1.intern(); System.out.println( s1 == s2);//true } public static void main(String[] args) { test01(); System.out.println("-----------------"); test02(); System.out.println("-----------------"); TestString t = new TestString(); t.test03(); System.out.println("-----------------"); test04(); } }
比较下面两个类,执行方法顺序不一样,结果不一样
先执行test01(),在执行test02(),结果是false false
1 package com.example.demo.test; 2 3 /** 4 * @description: 5 * @author: luguilin 6 * @date: 2022-02-25 11:14 7 **/ 8 public class TestString { 9 static void test01(){ 10 String s1 = new String("1")+new String("23"); 11 String s2 = "123"; 12 s1.intern(); 13 System.out.println( s1 == s2); //false 14 } 15 16 static void test02(){ 17 String s1 = new String("1")+new String("23"); 18 s1.intern(); 19 String s2 = "123"; 20 System.out.println( s1 == s2);// false 21 } 22 23 public static void main(String[] args) { 24 test01(); 25 System.out.println("-----------------"); 26 test02(); 27 System.out.println("-----------------"); 28 } 29 }
先执行test02(),在执行test01(),结果是true false
1 package com.example.demo.test; 2 3 /** 4 * @description: 5 * @author: luguilin 6 * @date: 2022-02-25 11:14 7 **/ 8 public class TestString { 9 static void test01(){ 10 String s1 = new String("1")+new String("23"); 11 String s2 = "123"; 12 s1.intern(); 13 System.out.println( s1 == s2); //false 14 } 15 16 static void test02(){ 17 String s1 = new String("1")+new String("23"); 18 s1.intern(); 19 String s2 = "123"; 20 System.out.println( s1 == s2);// false 21 } 22 23 public static void main(String[] args) { 24 test02(); 25 System.out.println("-----------------"); 26 test01(); 27 System.out.println("-----------------"); 28 } 29 }