JDK8中String的intern()方法實例詳細解讀
一、前言
String
字符串在我們日常開發(fā)中最常用的,當然還有他的兩個兄弟StringBuilder和StringBuilder
。他三個的區(qū)別也是面試中經常問到的,大家如果不知道,就要先去看看了哈!最近也是看周志明老師的深入JVM一書中寫到關于intern()
方法的介紹,小編也是以前沒在開發(fā)中用到。但是面試題還是很多的,所以特意研究了一天,寫下來記錄一下自己的收獲,希望也可以幫助到大家!!
二、圖文理解String創(chuàng)建對象
1.例子一
String str1 = "wang";
JVM在編譯階段會判斷字符串常量池中是否有 "wang" 這個常量對象如果有,str1直接指向這個常量的引用,如果沒有會在常量池里創(chuàng)建這個常量對象。
2.例子二
String str2 = "學" + "Java";
JVM編譯階段過編譯器優(yōu)化
后會把字符串常量直接合并成"學Java"
,所有創(chuàng)建對象時只會在常量池中創(chuàng)建1個對象。
3.例子三
String str3 = new String("學Java");
當代碼執(zhí)行到括號中的"學Java"的時候會檢測常量池中是否存在"學Java"這個對象
,如果不存在則在字符串常量池中創(chuàng)建一個對象。當整行代碼執(zhí)行完畢時會因為new關鍵字在堆中創(chuàng)建一個"學Java"對象,并把棧中的變量"str3"指向堆中的對象,如下圖所示。這也是為什么說通過new關鍵字在大部分情況下會創(chuàng)建出兩個字符串對象!
4.例子四
String str4 = "學Java"; String str5 = "學Java"; System.out.println(str4 == str5); // 如下圖得知為:true
第一行代碼:
JVM在編譯階段會判斷字符串常量池中是否有 "學Java" 這個常量對象如果有,str4直接指向這個常量的引用,如果沒有會在常量池里創(chuàng)建這個常量對象。
第二行代碼:
再創(chuàng)建"學Java",發(fā)現字符串常量池中存在了"學Java",所以直接將棧中的str5變量也指向字符串常量池中已存在的"學Java"對象
,從而避免重復創(chuàng)建對象,這也是字符串常量池存在的原因。
5.例子五
String str6 = new String("學") + new String("Java");
首先,會先判斷字符串常量池中是否存在"學"字符串對象,如果不存在則在字符串常量池中創(chuàng)建一個對象。當執(zhí)行到new關鍵字在堆中創(chuàng)建一個"學"字符串對象。后面的new String("Java"),也是這樣。
然后,當右邊完成時,會在堆中創(chuàng)建一個"學Java"字符串對象。并把棧中的變量"str6"指向堆中的對象。
總結:一句代碼創(chuàng)建了5個對象
,但是有兩個在堆中是沒有引用的,按照垃圾回收的可達性分析,他們是垃圾就是"學"、"Java"這倆垃圾
。
心得:
上面代碼進行反編譯:
String str6 = (new StringBuilder()).append(new String("\u5B66")) .append(new String("Java")).toString();
底層是一個StringBuilder在進行把兩個對象拼接在一起,最后棧中str6指向堆中的"學Java",其實是StringBuilder對象。
6.例子六
String str7 = new String("學Java"); String str8 = new String("學Java"); System.out.println(str7 == str8); // 如下圖得知為:false
執(zhí)行到第一行:
執(zhí)行到括號內的"學Java",會先判斷字符串常量池中是否存在"學Java"字符串對象,如果沒有則在字符串常量池中創(chuàng)建一個"學Java"字符串對象,執(zhí)行到new關鍵字時,在堆中創(chuàng)建一個"學Java"字符串對象,棧中的變量str7的引用指向堆中的"學Java"字符串對象。
執(zhí)行到第二行:
當執(zhí)行到第二行括號中的"學Java"時,先判斷常量池中是否有"學Java"字符串對象,因為第一行代碼已經將其創(chuàng)建,所以有的話就不創(chuàng)建了;執(zhí)行到new關鍵字時,在堆中創(chuàng)建一個"學Java"字符串對象,棧中的變量str8的引用指向堆中的"學Java"字符串對象。
三、深入理解intern()方法
1. 源碼查看
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // .... /** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <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> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern(); }
翻譯過來就是,當intern()方法被調用的時候,如果字符串常量池中已經存在這個字符串對象了,就返回常量池中該字符串對象的地址;如果字符串常量池中不存在,就在常量池中創(chuàng)建一個指向該對象堆中實例的引用,并返回這個引用地址。
2. 例子一
我們直接先把周志明老師的在深入JVM一書的例子:
String str1 = new StringBuilder("計算機").append("軟件").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2);
這段代碼在JDK 6中運行,會得到兩個false,而在JDK 7、8中運行,會得到一個true和一個false。產 生差異的原因是,在JDK 6中,intern()方法會把首次遇到的字符串實例復制到永久代的字符串常量池 中存儲,返回的也是永久代里面這個字符串實例的引用,而由StringBuilder創(chuàng)建的字符串對象實例在 Java堆上,所以必然不可能是同一個引用,結果將返回false。 而JDK 7(以及部分其他虛擬機,例如JRockit)的intern()方法實現就不需要再拷貝字符串的實例到永久代了,既然字符串常量池已經移到Java堆中,那只需要在常量池里記錄一下首次出現的實例引用即可,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個字符串實例就是同一個。而對str2比較返 回false,這是因為“java”(下面解釋)這個字符串在執(zhí)行String-Builder.toString()之前就已經出現過了,字符串常量 池中已經有它的引用,不符合intern()方法要求“首次遇到”的原則,“計算機軟件”這個字符串則是首次出現的,因此結果返回true。
java為什么已經存在了?
1.我們在一個類中輸入System
,然后點擊到這個方法中,方法內容如下:
public final class System { // ... private static void initializeSystemClass() { // ... sun.misc.Version.init(); // ... } // ... }
2.我們點擊上面的Version類,類內容如下:
public class Version { private static final String launcher_name = "java"; private static final String java_version = "1.8.0_121"; private static final String java_runtime_name = "Java(TM) SE Runtime Environment"; private static final String java_profile_name = ""; private static final String java_runtime_version = "1.8.0_121-b13"; private static boolean versionsInitialized; private static int jvm_major_version; private static int jvm_minor_version; private static int jvm_micro_version; private static int jvm_update_version; private static int jvm_build_number; private static String jvm_special_version; private static int jdk_major_version; private static int jdk_minor_version; private static int jdk_micro_version; private static int jdk_update_version; private static int jdk_build_number; private static String jdk_special_version; private static boolean jvmVersionInfoAvailable; public Version() { } public static void init() { System.setProperty("java.version", "1.8.0_121"); System.setProperty("java.runtime.version", "1.8.0_121-b13"); System.setProperty("java.runtime.name", "Java(TM) SE Runtime Environment"); } }
3.找到java關鍵字,所以上面的str2.intern() == str2
返回false。
private static final String launcher_name = "java";
我們開始例子和詳細解釋,發(fā)車了,大家坐好哦!
以下例子來自:原博客,解釋是為小編自己的理解。
3. 例子二
String str1 = new String("wang"); str1.intern(); String str2 = "wang"; System.out.println(str1 == str2); // false
執(zhí)行第一行代碼:
首先執(zhí)行到"wang",因為字符串常量池中沒有,則會在字符串常量池中創(chuàng)建"wang"字符串對象。
然后執(zhí)行到new關鍵字時,在堆中創(chuàng)建一個"wang"的對象,并把棧中的str1的引用指向"wang"對象。
執(zhí)行第二行代碼:
這里我們看到就是str1手動把"wang"放在字符串常量池中,但是發(fā)現字符串常量池中已經存在"wang"字符串對象,所以直接把已存在的引用返回。雖然str1.intern()指向了字符串常量池中的"wang",但是我們第四行代碼并沒有拿str1.intern()作比較,所以還是false。
執(zhí)行第三行代碼:
首先通過第一行代碼,字符串常量池中已經有"wang"字符串對象了,所以本行代碼只需要把棧中的str2變量指向字符串常量池中的"wang"即可。
執(zhí)行第四行代碼:
如上和下圖可見,我們的str1執(zhí)行堆中的"wang",str2指向的是字符串常量池中的"wang",肯定返回false。
4. 例子三
String str3 = new String("wang") + new String("zhen"); str3.intern(); String str4 = "wangzhen"; System.out.println(str3 == str4); // true
執(zhí)行到第一行代碼:
首先執(zhí)行到"wang"時,因為字符串常量池中沒有,則會在字符串常量池中創(chuàng)建一個"wang"字符串對象;
然后執(zhí)行到"zhen"時,因為字符串常量池中沒有,則會在字符串常量池中創(chuàng)建一個"zhen"字符串對象;
最后執(zhí)行到new關鍵字時,看到是兩個,但是底層字節(jié)碼文件反編譯的是使用如下可見,只會有一個StringBuilder對象生成,于是將棧中的str3的引用指向堆中的"wangzhen"對象。
String str3 = (new StringBuilder()).append(new String("wang")) .append(new String("zhen")).toString();
執(zhí)行到第二行代碼:
這里我們看到就是str3手動把"wangzhen"放在字符串常量池中,在字符串常量池中沒有找到"wangzhen",于是把str3 .intern()引用指向堆中的"wangzhen"的地址。現在str3和str3 .intern()一樣
執(zhí)行到第三行代碼:
判斷字符串常量池中是否存在"wangzhen"字符串對象,第二行代碼已經在字符串常量池中創(chuàng)建了"wangzhen",不過str4是指向str3中堆的引用(看圖就明白了)。
執(zhí)行到第四行代碼:
str3和str3 .intern()引用一樣,str3 .intern()和str4是一個,所以str3和str4相等。
5. 例子四
String str5 = new String("wang") + new String("zhen"); String str6 = "wangzhen"; str5.intern(); System.out.println(str5 == str6); // false
執(zhí)行到第一行代碼:
首先執(zhí)行到"wang"時,因為字符串常量池中沒有,則會在字符串常量池中創(chuàng)建一個"wang"字符串對象;
然后執(zhí)行到"zhen"時,因為字符串常量池中沒有,則會在字符串常量池中創(chuàng)建一個"zhen"字符串對象;
最后執(zhí)行到new關鍵字時,看到是兩個,但是底層字節(jié)碼文件反編譯的是使用如下可見,只會有一個StringBuilder對象生成,于是將棧中的str5的引用指向堆中的"wangzhen"對象。同上面的反編譯代碼
執(zhí)行到第二行代碼:
執(zhí)行到"wangzhen",判斷字符串常量池中是否存在"wangzhen",發(fā)現沒有,在字符串常量池中創(chuàng)建"wangzhen"字符串對象,然后把棧中的str6變量的引用指向"wangzhen"對象。
執(zhí)行到第三行代碼:
這里我們看到就是str5手動把"wangzhen"放在字符串常量池中,我們發(fā)現,在字符串常量池中已存在"wangzhen",于是str5 .intern()就是"wangzhen"對象的地址。我們還沒沒有收到返回值
如下圖,我們看到肯定返回false,此時str5.intern() == str6 (true)
6. 例子五
String str7 = new String("wang") + new String("zhen"); String str8 = "wangzhen"; System.out.println(str7.intern() == str8); // true System.out.println(str7 == str8); // false System.out.println(str8 == "wangzhen"); // true
執(zhí)行到第一行代碼:
同例子三和例子四的第一行代碼;
執(zhí)行到第二行代碼:
先判斷字符串常量池中是否存在"wangzhen"對象,發(fā)現沒有,我們在字符串常量池中創(chuàng)建"wangzhen"字符串對象;
執(zhí)行到第三行代碼:
執(zhí)行到str7.intern()這里,我們看到就是str7手動把"wangzhen"放在字符串常量池中,在字符串常量池中已結存在"wangzhen",于是把字符串常量池"wangzhen"的地址?,F在str7和str7 .intern()一樣
執(zhí)行到第四行代碼:
str7的指向為堆中的"wangzhen",而str8指向則為字符串常量池中的"wangzhen",故不相同,返回false。
執(zhí)行到第五行代碼:
str8指向則為字符串常量池中的"wangzhen",執(zhí)行"wangzhen",則把已存在的字符串常量池中"wangzhen"返回,故相同,返回true。
7. 例子六
String str9 = new String("wang") + new String("zhen"); System.out.println(str9.intern() == str9); // true System.out.println(str9 == "wangzhen"); // true
執(zhí)行到第一行代碼:
同上
執(zhí)行到第二行代碼:
執(zhí)行到str9.intern()這里,我們看到就是str9手動把"wangzhen"放在字符串常量池中,在字符串常量池中沒有"wangzhen",于是把str3 .intern()引用指向堆中的"wangzhen"的地址?,F在str9和str9.intern()一樣
執(zhí)行到第三行代碼:
str9指向堆內存中的"wangzhen",執(zhí)行到"wangzhen"時,發(fā)現字符串常量池中已存在,直接返回str9指向的引用即可,故返回true。
四、總結
經過這么多例子,大家應該明白了吧,還是要自己跟著例子進行換一下jvm內存圖,這樣就理解記憶,也不會輕易忘記!
到此這篇關于JDK8中String的intern()方法詳細解讀的文章就介紹到這了,更多相關JDK8中String的intern()內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot實現多實例crontab搶占定時任務(實例代碼)
這篇文章主要介紹了springboot實現多實例crontab搶占定時任務,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01java Swing JFrame框架類中setDefaultCloseOperation的參數含義與用法示例
這篇文章主要介紹了java Swing JFrame框架類中setDefaultCloseOperation的參數含義與用法,結合實例形式分析了Swing組件的JFrame框架類中setDefaultCloseOperation方法的簡單使用技巧,需要的朋友可以參考下2017-11-11MyBatis動態(tài)SQL中的trim標簽的使用方法
這篇文章主要介紹了MyBatis動態(tài)SQL中的trim標簽的使用方法,需要的朋友可以參考下2017-05-05