String s=a+b+c,到底创建了几个对象?
首先看一下這道常見(jiàn)的面試題,下面代碼中,會(huì)創(chuàng)建幾個(gè)字符串對(duì)象?
String?s="a"+"b"+"c";如果你比較一下Java源代碼和反編譯后的字節(jié)碼文件,就可以直觀的看到答案,只創(chuàng)建了一個(gè)String對(duì)象。
估計(jì)大家會(huì)有疑問(wèn)了,為什么源代碼中字符串拼接的操作,在編譯完成后會(huì)消失,直接呈現(xiàn)為一個(gè)拼接后的完整字符串呢?
這是因?yàn)樵诰幾g期間,應(yīng)用了編譯器優(yōu)化中一種被稱(chēng)為常量折疊(Constant Folding)的技術(shù),會(huì)將編譯期常量的加減乘除的運(yùn)算過(guò)程在編譯過(guò)程中折疊。編譯器通過(guò)語(yǔ)法分析,會(huì)將常量表達(dá)式計(jì)算求值,并用求出的值來(lái)替換表達(dá)式,而不必等到運(yùn)行期間再進(jìn)行運(yùn)算處理,從而在運(yùn)行期間節(jié)省處理器資源。
而上邊提到的編譯期常量的特點(diǎn)就是它的值在編譯期就可以確定,并且需要完整滿(mǎn)足下面的要求,才可能是一個(gè)編譯期常量:
被聲明為final
基本類(lèi)型或者字符串類(lèi)型
聲明時(shí)就已經(jīng)初始化
使用常量表達(dá)式進(jìn)行初始化
上面的前兩條比較容易理解,需要注意的是第三和第四條,通過(guò)下面的例子進(jìn)行說(shuō)明:
final?String?s1="hello?"+"Hydra"; final?String?s2=UUID.randomUUID().toString()+"Hydra";編譯器能夠在編譯期就得到s1的值是hello Hydra,不需要等到程序的運(yùn)行期間,因此s1屬于編譯期常量。而對(duì)s2來(lái)說(shuō),雖然也被聲明為final類(lèi)型,并且在聲明時(shí)就已經(jīng)初始化,但使用的不是常量表達(dá)式,因此不屬于編譯期常量,這一類(lèi)型的常量被稱(chēng)為運(yùn)行時(shí)常量。再看一下編譯后的字節(jié)碼文件中的常量池區(qū)域:
可以看到常量池中只有一個(gè)String類(lèi)型的常量hello Hydra,而s2對(duì)應(yīng)的字符串常量則不在此區(qū)域。對(duì)編譯器來(lái)說(shuō),運(yùn)行時(shí)常量在編譯期間無(wú)法進(jìn)行折疊,編譯器只會(huì)對(duì)嘗試修改它的操作進(jìn)行報(bào)錯(cuò)處理。
另外值得一提的是,編譯期常量與運(yùn)行時(shí)常量的另一個(gè)不同就是是否需要對(duì)類(lèi)進(jìn)行初始化,下面通過(guò)兩個(gè)例子進(jìn)行對(duì)比:
public?class?IntTest1?{public?static?void?main(String[]?args)?{System.out.println(a1.a);} } class?a1{static?{System.out.println("init?class");}public?static?int?a=1; }運(yùn)行上面的代碼,輸出:
init?class 1如果對(duì)上面進(jìn)行修改,對(duì)變量a添加final進(jìn)行修飾:
public?static?final?int?a=1;再次執(zhí)行上面的代碼,會(huì)輸出:
1可以看到在添加了final修飾后,兩次運(yùn)行的結(jié)果是不同的,這是因?yàn)樵谔砑觙inal后,變量a成為了編譯期常量,不會(huì)導(dǎo)致類(lèi)的初始化。另外,在聲明編譯器常量時(shí),final關(guān)鍵字是必要的,而static關(guān)鍵字是非必要的,上面加static修飾只是為了驗(yàn)證類(lèi)是否被初始化過(guò)。
我們?cè)倏磶讉€(gè)例子來(lái)加深對(duì)final關(guān)鍵字的理解,運(yùn)行下面的代碼:
public?static?void?main(String[]?args)?{final?String?h1?=?"hello";String?h2?=?"hello";String?s1?=?h1?+?"Hydra";String?s2?=?h2?+?"Hydra";System.out.println((s1?==?"helloHydra"));System.out.println((s2?==?"helloHydra")); }執(zhí)行結(jié)果:
true false代碼中字符串h1和h2都使用常量賦值,區(qū)別在于是否使用了final進(jìn)行修飾,對(duì)比編譯后的代碼,s1進(jìn)行了折疊而s2沒(méi)有,可以印證上面的理論,final修飾的字符串變量屬于編譯期常量。
再看一段代碼,執(zhí)行下面的程序,結(jié)果會(huì)返回什么呢?
public?static?void?main(String[]?args)?{String?h?="hello";final?String?h2?=?h;String?s?=?h2?+?"Hydra";System.out.println(s=="helloHydra"); }答案是false,因?yàn)殡m然這里字符串h2被final修飾,但是初始化時(shí)沒(méi)有使用編譯期常量,因此它也不是編譯期常量。
在上面的一些例子中,在執(zhí)行常量折疊的過(guò)程中都遵循了使用常量表達(dá)式進(jìn)行初始化這一原則,這里可能有的同學(xué)還會(huì)有疑問(wèn),到底什么樣才能算得上是常量表達(dá)式呢?在Oracle官網(wǎng)的文檔中,列舉了很多種情況,下面對(duì)常見(jiàn)的情況進(jìn)行列舉(除了下面這些之外官方文檔上還列舉了不少情況,如果有興趣的話,可以自己查看):
基本類(lèi)型和String類(lèi)型的字面量
基本類(lèi)型和String類(lèi)型的強(qiáng)制類(lèi)型轉(zhuǎn)換
使用+或-或!等一元運(yùn)算符(不包括++和--)進(jìn)行計(jì)算
使用加減運(yùn)算符+、-,乘除運(yùn)算符*、 / 、% 進(jìn)行計(jì)算
使用移位運(yùn)算符 >>、 <<、 >>>進(jìn)行位移操作
……
字面量(literals)是用于表達(dá)源代碼中一個(gè)固定值的表示法,在Java中創(chuàng)建一個(gè)對(duì)象時(shí)需要使用new關(guān)鍵字,但是給一個(gè)基本類(lèi)型變量賦值時(shí)不需要使用new關(guān)鍵字,這種方式就可以被稱(chēng)為字面量。Java中字面量主要包括了以下類(lèi)型的字面量:
//整數(shù)型字面量: long?l=1L; int?i=1;//浮點(diǎn)類(lèi)型字面量: float?f=11.1f; double?d=11.1;//字符和字符串類(lèi)型字面量: char?c='h'; String?s="Hydra";//布爾類(lèi)型字面量: boolean?b=true;當(dāng)我們?cè)诖a中定義并初始化一個(gè)字符串對(duì)象后,程序會(huì)在常量池(constant pool)中緩存該字符串的字面量,如果后面的代碼再次用到這個(gè)字符串的字面量,會(huì)直接使用常量池中的字符串字面量。
除此之外,還有一類(lèi)比較特殊的null類(lèi)型字面量,這個(gè)類(lèi)型的字面量只有一個(gè)就是null,這個(gè)字面量可以賦值給任意引用類(lèi)型的變量,表示這個(gè)引用類(lèi)型變量中保存的地址為空,也就是還沒(méi)有指向任何有效的對(duì)象。
那么,如果不是使用的常量表達(dá)式進(jìn)行初始化,在變量的初始化過(guò)程中引入了其他變量(且沒(méi)有被final修飾)的話,編譯器會(huì)怎樣進(jìn)行處理呢?我們下面再看一個(gè)例子:
public?static?void?main(String[]?args)?{String?s1="a";String?s2=s1+"b";String?s3="a"+"b";System.out.println(s2=="ab");System.out.println(s3=="ab"); }結(jié)果打印:
false true為什么會(huì)出現(xiàn)不同的結(jié)果?在Java中,String類(lèi)型在使用==進(jìn)行比較時(shí),是判斷的引用是否指向堆內(nèi)存中的同一塊地址,出現(xiàn)上面的結(jié)果那么說(shuō)明指向的不是內(nèi)存中的同一塊地址。
通過(guò)之前的分析,我們知道s3會(huì)進(jìn)行常量折疊,引用的是常量池中的ab,所以相等。而字符串s2在進(jìn)行拼接時(shí),表達(dá)式中引用了其他對(duì)象,不屬于編譯期常量,因此不能進(jìn)行折疊。
那么,在沒(méi)有常量折疊的情況下,為什么最后返回的是false呢?我們看一下這種情況下,編譯器是如何實(shí)現(xiàn),先執(zhí)行下面的代碼:
public?static?void?main(String[]?args)?{String?s1="my?";String?s2="name?";String?s3="is?";String?s4="Hydra";String?s=s1+s2+s3+s4; }然后使用javap對(duì)字節(jié)碼文件進(jìn)行反編譯,可以看到在這一過(guò)程中,編譯器同樣會(huì)進(jìn)行優(yōu)化:
可以看到,雖然我們?cè)诖a中沒(méi)有顯示的調(diào)用StringBuilder,但是在字符串拼接的場(chǎng)景下,Java編譯器會(huì)自動(dòng)進(jìn)行優(yōu)化,新建一個(gè)StringBuilder對(duì)象,然后調(diào)用append方法進(jìn)行字符串的拼接。而在最后,調(diào)用了StringBuilder的toString方法,生成了一個(gè)新的字符串對(duì)象,而不是引用的常量池中的常量。這樣,也就能解釋為什么在上面的例子中,s2=="ab"會(huì)返回false了。
本文代碼基于Java 1.8.0_261-b12 版本測(cè)試
公眾號(hào)后臺(tái)回復(fù)
"面試"---領(lǐng)取大廠面試資料
"導(dǎo)圖"---領(lǐng)取24張Java后端學(xué)習(xí)筆記導(dǎo)圖
"架構(gòu)"---領(lǐng)取29本java架構(gòu)師電子書(shū)籍
"實(shí)戰(zhàn)"---領(lǐng)取springboot實(shí)戰(zhàn)項(xiàng)目
關(guān)注公眾號(hào)
有趣、深入、直接
與你聊聊技術(shù)
覺(jué)得有用,一鍵四連吧~
總結(jié)
以上是生活随笔為你收集整理的String s=a+b+c,到底创建了几个对象?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一次字节面试,被二叉树的层序遍历捏爆了
- 下一篇: 两个半月!出差终于结束啦