java 中的intern()方法
https://www.bilibili.com/video/BV1PJ411n7xZ?p=127&vd_source=d52fb7546f3e6962911bc7cc32990c21

前言
最近遇到一个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 }




浙公网安备 33010602011771号