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 }
复制代码
posted on 2023-03-21 17:00  云淡风轻博客  阅读(331)  评论(0编辑  收藏  举报