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 }