Java字符串常量池和intern方法解析
這篇文章,來(lái)討論一下Java中的字符串常量池以及Intern方法.這里我們主要討論的是jdk1.7,jdk1.8版本的實(shí)現(xiàn).
字符串常量池
在日常開發(fā)中,我們使用字符串非常的頻繁,我們經(jīng)常會(huì)寫下類似如下的代碼:
String s = "abc"; String str = s + "def";
通常,我們一般不會(huì)這么寫:String s = new String("jkl"),但其實(shí)這么寫和上面的寫法還是有很多區(qū)別的.
先思考一個(gè)問題,為什么要有字符串常量池這種概念?原因是字符串常量既然是不變的,那么完全就可以復(fù)用它,而不用再重新去浪費(fèi)空間存儲(chǔ)一個(gè)完全相同的字符串.字符串常量池是用于存放字符串常量的地方,在Java7和Java8中字符串常量池是堆的一部分.
假如我們有如下代碼:
String s = "abc"; String s1 = s + "def"; String s2 = new String("abc"); String s3 = new String("def");
那么從內(nèi)存分配的角度上看,最終會(huì)有哪些字符串生成呢,首先我先給出一張圖來(lái)代表最終的結(jié)論,然后再分析一下具體的原因:
現(xiàn)在來(lái)依次分析上面的代碼的執(zhí)行流程:
執(zhí)行String s = "abc",此時(shí)遇到"abc"這個(gè)字符串常量,會(huì)在字符串常量池中完成分配,并且會(huì)將引用賦值給s,因此這條語(yǔ)句會(huì)在字符串常量池中分配一個(gè)"abc".(這里其實(shí)沒有空格,是因?yàn)樯晌恼聲r(shí)出現(xiàn)了空格,下文中如果出現(xiàn)同樣情況,請(qǐng)忽略空格)
執(zhí)行String s1 = s + "def",其實(shí)這個(gè)語(yǔ)句看似簡(jiǎn)單,實(shí)則另有玄機(jī),它其實(shí)最終編譯而成的代碼是這樣的:String s1 = new StringBuilder("abc").append("def").toString().首先在這個(gè)語(yǔ)句中有兩個(gè)字符串常量:"abc"和"def",所以在字符串常量池中應(yīng)該放置"abc"和"def",但是上個(gè)步驟已經(jīng)有"abc"了,所以只會(huì)放置"def".另外,new StringBuilder("abc")這個(gè)語(yǔ)句相當(dāng)于在堆上分配了一個(gè)對(duì)象,如果是new出來(lái)的,是在堆上分配字符串,是無(wú)法共享字符串常量池里面的字符串的,也就是說(shuō)分配到堆上的字符串都會(huì)有新的內(nèi)存空間. 最后toString()也是在堆中分配對(duì)象(可以從源碼中看到這個(gè)動(dòng)作),最終相當(dāng)于執(zhí)行了new String("abcdef");所以總結(jié)起來(lái),這條語(yǔ)句分析起來(lái)還是挺麻煩的,它分配了以下對(duì)象:
- 在字符串常量池分配"abc",但本來(lái)就有一個(gè)"abc"了,所以不需要分配
- 在字符串常量池中分配“def"
- 在堆中分配了"abc"
- 在堆中分配了"abcdef"
執(zhí)行String s2 = new String("abc").首先有個(gè)字符串常量"abc",需要分配到字符串常量池,但是字符串常量池中已經(jīng)有"abc"了,所以無(wú)需分配.因此new String("abc")最終在堆上分配了一個(gè)"abc".所以總結(jié)起來(lái)就是,在堆中分配了一個(gè)"abc"
執(zhí)行String s3 = new String("def");.首先有個(gè)字符串常量"def",需要分配到字符串常量池,但是字符串常量池中已經(jīng)有"def"了,所以無(wú)需分配.因此new String("def")最終在堆上分配了一個(gè)"def".所以總結(jié)起來(lái)就是,在堆中分配了一個(gè)"def"。
總結(jié)起來(lái),全部語(yǔ)句執(zhí)行后分配的對(duì)象如下:
- 在堆中分配了兩個(gè)"abc",一個(gè)"abcdef",一個(gè)"def"
- 在字符串常量池中分配了一個(gè)"abc",一個(gè)"def"
也就是圖中所表示的這些對(duì)象,如果明白了對(duì)象是如何分配的,我們就可以分析以下代碼的結(jié)果:
String s = "abc"; String s1 = s + "def"; String s2 = new String("abc"); String s3 = new String("def"); String s4 = "abcdef"; String s5 = "abc"; System.out.println(s == s2); //false 前者引用的對(duì)象在字符串常量池 后者在堆上 System.out.println(s == s5);; //true 都引用了字符串常量池中的"abc" System.out.println(s1 == s4); //false 前者引用的對(duì)象在字符串常量池,后者在堆上
intern方法
在字符串對(duì)象中,有一個(gè)intern方法.在jdk1.7,jdk1.8中,它的定義是如果調(diào)用這個(gè)方法時(shí),在字符串常量池中有對(duì)應(yīng)的字符串,那么返回字符串常量池中的引用,否則返回調(diào)用時(shí)相應(yīng)對(duì)象的引用,也就是說(shuō)intern方法在jdk1.7,jdk1.8中只會(huì)復(fù)用某個(gè)字符串的引用,這個(gè)引用可以是對(duì)堆內(nèi)存中字符串中的引用,也可能是對(duì)字符串常量池中字符串的引用.這里通過一個(gè)例子來(lái)說(shuō)明,假如我們有下面這段代碼:
String str = new String("abc"); String str2 = str.intern(); String str3 = new StringBuilder("abc").append("def").toString(); String str4 = str3.intern(); System.out.println(str == str2); System.out.println(str3 == str4);
那么str2和str以及str3和str4是否相等呢?如果理解了上面對(duì)字符串常量池的分析,那么我們可以明白在這段代碼中,字符串在內(nèi)存中是這么分配的:
- 在堆中分配兩個(gè)"abc",一個(gè)“abcdef"
- 在字符串常量池中分配一個(gè)"def",一個(gè)"abc"
當(dāng)執(zhí)行String str2 = str.intern();時(shí),會(huì)先從字符串常量池中尋找是否有對(duì)應(yīng)的字符串,此時(shí)在字符串常量池中有一個(gè)"abc",那么str2就指向字符串常量池中的"abc",而str是new出來(lái)的,指向的是堆中的"abc",所以str不等于str2;
當(dāng)執(zhí)行String str4 = str3.intern();會(huì)先從字符串常量池中尋找"abcdef",此時(shí)字符串常量池中并沒有"abcdef",因此str4會(huì)指向堆中的"abcdef",因此str3等于str4,我們會(huì)發(fā)現(xiàn)一個(gè)有意思的地方:如果將第三句改成String str3 = new StringBuilder("abcdef").toString();,也就是把a(bǔ)ppend后面的字符串和前面的字符串做一個(gè)拼接,那么結(jié)果就會(huì)變成str3不等于str4.所以這兩種寫法的區(qū)別還是挺大的.
要注意的是,在jdk1.6中intern的定義是如果字符串常量池中沒有對(duì)應(yīng)的字符串,那么就在字符串常量池中創(chuàng)建一個(gè)字符串,然后返回字符串常量池中的引用,也就是說(shuō)在jdk1.6中,intern方法返回的對(duì)象始終都是指向字符串常量池的.如果上面的代碼在jdk1.6中運(yùn)行,那么就會(huì)得到兩個(gè)false,原因如下:
- 當(dāng)執(zhí)行String str2 = str.intern();時(shí),會(huì)先從字符串常量池中尋找是否有對(duì)應(yīng)的字符串,此時(shí)在字符串常量池中有一個(gè)"abc",那么str2就指向字符串常量池中的"abc",而str是new出來(lái)的,指向的是堆中的"abc",所以str不等于str2;
- 當(dāng)執(zhí)行String str4 = str3.intern();會(huì)先從字符串常量池中尋找"abcdef",此時(shí)字符串常量池中并沒有"abcdef",因此執(zhí)行intern方法會(huì)在字符串常量池中分配"abcdef",然后str4最終等于這個(gè)字符串的引用,因此str3不等于str4,因?yàn)樯厦娴膕tr3指向堆,而str4指向字符串常量池,所以兩者一定不會(huì)相等.
在深入理解JVM虛擬機(jī)一書中,就有類似的代碼:
String str1 = new StringBuilder("計(jì)算機(jī)").append("軟件").toString(); System.out.println(str1 == str1.intern()); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2 == str2.intern());
在jdk1.6中,兩個(gè)判斷都為false.因?yàn)閟tr1和str2都指向堆,而intern方法得出來(lái)的引用都指向字符串常量池,所以不會(huì)相等,和上面敘述的結(jié)論是一樣的.在jdk1.7中,第一個(gè)是true,第二個(gè)是false.道理其實(shí)也和上述所講的是一樣的,對(duì)于第一個(gè)語(yǔ)句,最終會(huì)在堆上創(chuàng)建一個(gè)"計(jì)算機(jī)軟件"的字符串,執(zhí)行str1.intern()方法時(shí),先在字符串常量池中尋找字符串,但沒有找到,所以會(huì)直接引用堆上的這個(gè)"計(jì)算機(jī)軟件",因此第一個(gè)語(yǔ)句會(huì)返回true,因?yàn)樽罱K都是指向堆.而對(duì)于第三個(gè)語(yǔ)句,因?yàn)楹偷谝粋€(gè)語(yǔ)句差不多,按理說(shuō)最終比較也應(yīng)該返回true.但實(shí)際上,str2.intern方法執(zhí)行的時(shí)候,在字符串常量池中是可以找到"java"這個(gè)字符串的,這是因?yàn)樵贘ava初始化環(huán)境去加載類的時(shí)候(執(zhí)行main方法之前),已經(jīng)有一個(gè)叫做"java"的字符串進(jìn)入了字符串常量池,因此str2.intern方法返回的引用是指向字符串常量池的,所以最終判斷的結(jié)果是false,因?yàn)橐粋€(gè)指向堆,一個(gè)指向字符串常量池.
總結(jié)
從上面的分析看來(lái),字符串常量池并不像是那種很簡(jiǎn)單的概念,要深刻理解字符串常量池,至少需要理解以下幾點(diǎn):
- 理解字符串會(huì)在哪個(gè)內(nèi)存區(qū)域存放
- 理解遇到字符串常量會(huì)發(fā)生什么
- 理解new String或者是new StringBuilder產(chǎn)生的對(duì)象會(huì)在哪里存放
- 理解字符串拼接操作+最終編譯出來(lái)的語(yǔ)句是什么樣子的
- 理解toString方法會(huì)發(fā)生什么
這幾點(diǎn)都在本文章中覆蓋了,相信理解了這幾點(diǎn)之后一定對(duì)字符串常量池有一個(gè)更深刻的理解.其實(shí)這篇文章的編寫原因是因?yàn)殚喿x深入理解JVM虛擬機(jī)這本書的例子時(shí)突然發(fā)現(xiàn)作者所說(shuō)的和我所想的是不一樣的,但是書上可能對(duì)這方面沒有展開敘述,所以我去查了點(diǎn)資料,然后寫了一些代碼來(lái)驗(yàn)證,最終決定寫一篇文章來(lái)記錄一下自己的理解,在編寫代碼過程中,還發(fā)現(xiàn)了一個(gè)分析對(duì)象內(nèi)存地址的類庫(kù),我放在參考資料中了.
參考資料
https://www.baeldung.com/java-object-memory-address查看java對(duì)象內(nèi)存地址
到此這篇關(guān)于Java字符串常量池和intern方法解析的文章就介紹到這了,更多相關(guān)Java字符串常量池和intern方法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實(shí)現(xiàn)微信支付接口調(diào)用及回調(diào)函數(shù)(商戶參數(shù)獲取)
本文詳細(xì)介紹了使用SpringBoot實(shí)現(xiàn)微信支付接口調(diào)用及回調(diào)函數(shù)的步驟,提供了代碼實(shí)現(xiàn)的具體步驟和工具類的創(chuàng)建,感興趣的朋友跟隨小編一起看看吧2024-11-11Java8的Lambda遍歷兩個(gè)List匹配數(shù)據(jù)方式
這篇文章主要介紹了Java8的Lambda遍歷兩個(gè)List匹配數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03關(guān)于JDBC的簡(jiǎn)單封裝(實(shí)例講解)
下面小編就為大家?guī)?lái)一篇關(guān)于JDBC的簡(jiǎn)單封裝(實(shí)例講解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-08-08如何修改logback.xml配置文件在resource以外的位置
這篇文章主要介紹了如何修改logback.xml配置文件在resource以外的位置,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2021-02-02springboot配合Thymeleaf完美實(shí)現(xiàn)遍歷功能
Thymeleaf顯然是一個(gè)開發(fā)頁(yè)面的技術(shù),現(xiàn)在各種前端技術(shù)層出不窮,比如現(xiàn)在主流的Vue、React、AngularJS等。這篇文章主要介紹了springboot配合Thymeleaf完美實(shí)現(xiàn)遍歷,需要的朋友可以參考下2021-09-09Java多線程生產(chǎn)者消費(fèi)者模式實(shí)現(xiàn)過程解析
這篇文章主要介紹了Java多線程生產(chǎn)者消費(fèi)者模式實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Java反射之Call stack introspection詳解
這篇文章主要介紹了Java反射之Call stack introspection詳解,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11Java操作透明圖片并保持背景透明的實(shí)現(xiàn)
這篇文章主要介紹了Java操作透明圖片并保持背景透明的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11