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 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM