1. 流程圖詳解 new String("abc") 創(chuàng)建了幾個(gè)字符串對(duì)象

        共 4490字,需瀏覽 9分鐘

         ·

        2022-05-20 01:59

        前言

        這道題是我之前的面試題文章《Java 基礎(chǔ)高頻面試題(2021年最新版)》里的第10題,今天通過(guò)字節(jié)碼和流程圖來(lái)跟大家詳解一下完整的執(zhí)行過(guò)程。

        同時(shí)也會(huì)涉及一些字符串常量池的相關(guān)知識(shí),這塊內(nèi)容網(wǎng)上現(xiàn)在的說(shuō)法有太多錯(cuò)誤了。

        本文內(nèi)容有視頻版本,喜歡看視頻的同學(xué)可以直接通過(guò)下面的二維碼觀看。如果你對(duì)文章的內(nèi)容有疑惑,可以先看視頻的對(duì)應(yīng)內(nèi)容,視頻可能講的會(huì)更細(xì)一點(diǎn)。


        答案

        首先直接說(shuō)答案,一個(gè)比較合理的答案是:一個(gè)或者兩個(gè)字符串對(duì)象,通常這個(gè)也是面試官想要聽(tīng)到的答案。

        首先,new string 這邊由于 new 關(guān)鍵字,所以這邊肯定會(huì)在堆中新建一個(gè)字符串對(duì)象。

        其次,如果字符串常量池中不存在 jionghui(equals比較)這個(gè)字符串,則會(huì)在字符串常量池中創(chuàng)建一個(gè)字符串對(duì)象。

        注意:這邊說(shuō)的在字符串常量池創(chuàng)建對(duì)象,最終對(duì)象還是在堆中創(chuàng)建,字符串常量池只放引用。


        例子1:String?str1?=?new?String("jionghui")?

        本例子按照字符串常量池中不存在 jionghui 字符串來(lái)說(shuō)。

        該代碼編譯后其字節(jié)碼如下:

         0 new #2 String> 3 dup 4 ldc #3  6 invokespecial #4 String. : (Ljava/lang/String;)V>?9?astore_110?return

        接下來(lái)我們解釋下這些字節(jié)碼

        1)#2、#3、#4

        字節(jié)碼中這些帶 # 號(hào)的數(shù)字,是我們常量池里面的符號(hào)引用,這些符號(hào)引用會(huì)在類(lèi)加載的解析階段被解析為直接引用,直接引用可以理解為就是對(duì)象在內(nèi)存中的地址。

        這些符號(hào)引用對(duì)應(yīng)的內(nèi)容在后面已經(jīng)給你列出來(lái)了。

        #2 這邊對(duì)應(yīng)的是 java.lang.String 的 Class 類(lèi)

        #3 對(duì)應(yīng)的 jionghui 字符串

        #4 對(duì)應(yīng)的 String 的初始化方法

        2)new

        new 關(guān)鍵字就是新建對(duì)象的意思,這邊相當(dāng)于會(huì)新建一個(gè) String 對(duì)象,但是此時(shí)還未初始化,是一個(gè)空對(duì)象。同時(shí),這個(gè)字節(jié)碼會(huì)將創(chuàng)建的對(duì)象的引用存放到操作數(shù)棧的棧頂。

        執(zhí)行完該指令后的結(jié)構(gòu):


        3)dup

        復(fù)制的意思,這邊就是復(fù)制一份棧頂?shù)脑亍?/span>

        這邊在棧頂復(fù)制一個(gè) String 的引用是因?yàn)楹罄m(xù)調(diào)用 String 的初始化方法會(huì)消耗掉棧里的一個(gè)引用,所以這邊提前復(fù)制一份出來(lái),最后才有引用可以賦值給局部變量表的str1。

        執(zhí)行完該指令后的結(jié)構(gòu):


        4)ldc

        將int、float或String型常量值從常量池中推送至棧頂,這個(gè)地方 ldc 指令會(huì)附帶另外一個(gè)功能:觸發(fā)符號(hào)引用解析為直接引用。

        我們上文說(shuō)過(guò)符號(hào)引用會(huì)在解析階段被解析成直接引用,但是有一些特例。字符串對(duì)象就是一個(gè)特例,字符串對(duì)象不會(huì)在解析階段就將符號(hào)引用解析成直接引用,而是等到某個(gè)“合適的時(shí)機(jī)”才去解析,這邊的 ldc 就是這個(gè)時(shí)機(jī)。

        PS:下面的例子5會(huì)驗(yàn)證這個(gè)說(shuō)法。

        因此 ldc 在這邊會(huì)做兩件事:

        1、判斷符號(hào)引用是否已經(jīng)解析成了直接引用,如果沒(méi)有,則會(huì)進(jìn)行解析:判斷 jionghui 字符串是否已經(jīng)在字符串常量池存在,如果存在則將符號(hào)引用解析成字符串常量池的引用;如果不存在,則會(huì)在字符串常量池中創(chuàng)建一個(gè)jionghui 字符串對(duì)象,然后同樣將符號(hào)引用解析成字符串常量池的引用。

        2、將對(duì)應(yīng)的字符串常量池推送到棧頂。

        執(zhí)行完該指令后的結(jié)構(gòu):


        5)invokespecial

        調(diào)用超類(lèi)構(gòu)造方法,實(shí)例初始化方法,私有方法

        在這邊用于調(diào)用 String 的初始化方法,我們上面通過(guò) new 關(guān)鍵詞創(chuàng)建的是個(gè)空對(duì)象,還未進(jìn)行初始化。

        這邊初始化會(huì)使用到我們棧頂?shù)膬蓚€(gè)元素,一個(gè)元素指向我們要初始化的對(duì)象,另一個(gè)元素指向我們初始化使用的參數(shù)。

        這邊初始化完畢后,這個(gè)空字符串對(duì)象會(huì)被初始化成 jionghui 字符串對(duì)象。

        執(zhí)行完該指令后的結(jié)構(gòu):

        6)astore_1

        將棧頂引用元素存到指定本地變量。

        這邊最后將棧頂?shù)倪@個(gè)引用存放到本地變量表找那個(gè)的 str1 變量。

        執(zhí)行完該指令后的結(jié)構(gòu):


        執(zhí)行完畢后,最終如上圖所示。可以看到最終就是創(chuàng)建了兩個(gè)對(duì)象,一個(gè)是是通過(guò)new string 創(chuàng)建出來(lái)的這個(gè)對(duì)象,它的引用被復(fù)賦值給 str1,另外一個(gè)是在常量池里創(chuàng)建的字符串對(duì)象。


        例子2:String str2 = "jionghui"

        這個(gè)例子就是例子1的簡(jiǎn)版,去掉了 new String 的過(guò)程,其他基本一樣。

        執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:


        例子3:String str3 = "jiong" + "hui";

        該例子在編譯后,這2個(gè)字符串會(huì)被自動(dòng)合并成 jionghui,所以最終跟例子2完全一樣,編譯后的字節(jié)碼都是完全一樣的。

        執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:


        例子4:String str4 = new String("jiong") + "hui"

        核心流程如下:

        1)雙引號(hào)修飾的字面量?jiong 和 hui 分別會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象

        2)new String 關(guān)鍵字會(huì)再創(chuàng)建一個(gè) jiong 字符串對(duì)象

        3)最后這個(gè)字符串拼接,這個(gè)地方不看字節(jié)碼的話很難看出究竟是怎么拼接的,通過(guò)字節(jié)碼一下子就看出來(lái)了,這邊是通過(guò) StringBuilder 來(lái)進(jìn)行字符串的拼接操作,先創(chuàng)建了一個(gè) StringBuilder,然后 append("jiong"),然后 append("hui"),最后執(zhí)行 toString 返回,這邊 toString 底層是通過(guò) new String 方法返回,所以最終這邊拼接也會(huì)創(chuàng)建一個(gè)新的字符串。

        字節(jié)碼如下:用到的命令都是上文提過(guò)的。

         0 new #2  3 dup 4 invokespecial #3  : ()V> 7 new #4 10 dup11 ldc #5 13 invokespecial #6  : (Ljava/lang/String;)V>16 invokevirtual #7 19 ldc #8 21 invokevirtual #7 24 invokevirtual #9 27 astore_128?return


        執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:


        例子5:intern 測(cè)試1

        String str5 = new String("1") + new String("1");str5.intern();String str6 = "11";System.out.println(str5 == str6);

        intern:如果字符串常量池中存在當(dāng)前字符串, 則返回常量池中的字符串引用。否則, 將該字符串放入常量池,然后返回該字符串對(duì)象的引用。

        intern 在 JDK6 和 JDK7 及之后的版本有些不同,這邊會(huì)簡(jiǎn)單說(shuō)下不同的地方。

        JDK7下的核心流程如下:

        1)雙引號(hào)修飾的字面量 1?會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象,這邊有2個(gè)字面量 1,但是只會(huì)創(chuàng)建1次,另一個(gè)直接復(fù)用

        2)兩個(gè) new String 創(chuàng)建了2個(gè)字符串對(duì)象?1

        3)字符串拼接通過(guò) StringBuilder?創(chuàng)建出1個(gè)新的字符串對(duì)象 11,并將引用賦值給 str5

        4)str5 調(diào)用 intern 方法,檢查到字符串常量池還沒(méi)有字符串11,則將字符串對(duì)象放入常量池,此時(shí)字符串常量池中的 11 就是 str5 指向的字符串對(duì)象

        5)雙引號(hào)修飾的字面量?11?檢查到字符串常量池中已經(jīng)存在字符串 11,則直接使用字符串常量池中的對(duì)象,所以 str6 被賦值為字符串常量池中的對(duì)象引用,也就是 str5的引用

        6)輸出結(jié)果為 true

        執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:



        而 JDK6 下的流程有什么不同呢,主要在于 JDK6 版本還存在永久代的概念,字符串常量池指向的字符串對(duì)象在 JDK6 中是在永久代創(chuàng)建的,JDK7才被移動(dòng)到堆中。

        所以當(dāng)執(zhí)行 str5.intern 時(shí),發(fā)現(xiàn)永久代中沒(méi)有字符串11,則會(huì)在永久代創(chuàng)建字符串對(duì)象11,后續(xù)的 str6 也是指向永久代的字符串對(duì)象。所以,此時(shí) str5 和 str6 指向的不同對(duì)象。

        因此,JDK6 的輸出結(jié)果為 false。

        執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:



        驗(yàn)證字符串對(duì)象在運(yùn)行中在解析符號(hào)引用

        這個(gè)例子還能驗(yàn)證我們上面說(shuō)的:字符串的符號(hào)引用在運(yùn)行階段才被解析成直接引用的說(shuō)法。

        我們假設(shè)字符串的符號(hào)引用也是在類(lèi)加載的解析階段就解析成直接引用了,那么這個(gè)例子的流程如下(JDK7及之后版本):

        1)解析階段,雙引號(hào)修飾的字面量 1 和 11 會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象

        2)兩個(gè) new String 創(chuàng)建了2個(gè)字符串對(duì)象?1

        3)字符串拼接通過(guò) StringBuilder?創(chuàng)建出1個(gè)新的字符串對(duì)象 11,并將引用賦值給 str5

        4)str5 調(diào)用 intern 方法,檢查到字符串常量池存在字符串11,則不做任何操作

        5)str6 被賦值為字符串常量池中的對(duì)象引用,此時(shí) str6 和 str5 指向的是不同的字符串對(duì)象

        6)輸出結(jié)果為 false

        本例在 JDK7及之后版本的輸出結(jié)果為 true,驗(yàn)證了我們的說(shuō)法。

        字符串常量池中的字符串對(duì)象使用懶加載在 JVM 源碼中是有明確注釋的,同時(shí) R 大也在某論壇上說(shuō)過(guò)。


        例子6:intern 測(cè)試2

        這個(gè)例子就是將例子5的2和3行代碼調(diào)換了下順序,驗(yàn)證一下 intern 方法的返回值。

        String str7 = new String("1") + new String("1");String str8 = "11";String str9 = str7.intern();System.out.println(str7 == str8);System.out.println(str8 == str9);

        核心流程如下:

        1)雙引號(hào)修飾的字面量 1?會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象,這邊有2個(gè)字面量 1,但是只會(huì)創(chuàng)建1次,另一個(gè)直接復(fù)用

        2)兩個(gè) new String 創(chuàng)建了2個(gè)字符串對(duì)象?1

        3)字符串拼接通過(guò) StringBuilder?創(chuàng)建出1個(gè)新的字符串對(duì)象 11,并將引用賦值給 str7

        3)雙引號(hào)修飾的字面量 11 會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象,并將引用賦值給 str8

        4)str7 調(diào)用 intern 方法,檢查到字符串常量池存在字符串11,則不做任何操作,同時(shí)返回字符串常量池的引用,并賦值給 str9,也就是 str8 指向的引用

        5)輸出結(jié)果為 false 和 true


        執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:



        推薦閱讀

        小白也看得懂的 I/O 多路復(fù)用解析

        外包公司能去嗎?進(jìn)了外包如何翻盤(pán)?

        全網(wǎng)最實(shí)用的 IDEA Debug 調(diào)試技巧(超詳細(xì)案例)

        Java 基礎(chǔ)高頻面試題(2021年最新版)

        Java 集合框架高頻面試題(2021年最新版)

        面試必問(wèn)的 Spring,你懂了嗎?

        面試必問(wèn)的 MySQL,你懂了嗎?

        最近我將面試:阿里、字節(jié)、美團(tuán)、快手、拼多多等大廠的高頻面試整理出來(lái),并按大廠的標(biāo)準(zhǔn)給出自己的解析。

        群里有不少同學(xué)看完拿下了阿里、美團(tuán)等大廠 Offer,希望能助你一臂之力,早日拿下大廠 Offer。

        獲取方式:關(guān)注公眾號(hào)回復(fù)【面試】即可領(lǐng)取,更多大廠面試真題解析 PDF 整理中。

        瀏覽 60
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 激情片91 | 久久精品在线视频 | 国产精品永久久久久久久久久 | 国产精品久久久久久久久久精爆 | 亚洲中文字幕在线观看视频 |