String s="a"+"b"+"c"牍颈,到底創(chuàng)建了幾個對象?

首先看一下這道常見的面試題琅关,下面代碼中煮岁,會創(chuàng)建幾個字符串對象?

String s="a"+"b"+"c";

如果你比較一下Java源代碼和反編譯后的字節(jié)碼文件涣易,就可以直觀的看到答案画机,只創(chuàng)建了一個String對象。

image

估計大家會有疑問了都毒,為什么源代碼中字符串拼接的操作色罚,在編譯完成后會消失,直接呈現(xiàn)為一個拼接后的完整字符串呢账劲?

這是因為在編譯期間戳护,應(yīng)用了編譯器優(yōu)化中一種被稱為常量折疊(Constant Folding)的技術(shù),會將編譯期常量的加減乘除的運算過程在編譯過程中折疊瀑焦。編譯器通過語法分析腌且,會將常量表達式計算求值,并用求出的值來替換表達式榛瓮,而不必等到運行期間再進行運算處理铺董,從而在運行期間節(jié)省處理器資源。

而上邊提到的編譯期常量的特點就是它的值在編譯期就可以確定禀晓,并且需要完整滿足下面的要求精续,才可能是一個編譯期常量:

  • 被聲明為final
  • 基本類型或者字符串類型
  • 聲明時就已經(jīng)初始化
  • 使用常量表達式進行初始化

上面的前兩條比較容易理解,需要注意的是第三和第四條粹懒,通過下面的例子進行說明:

final String s1="hello "+"Hydra";
final String s2=UUID.randomUUID().toString()+"Hydra";

編譯器能夠在編譯期就得到s1的值是hello Hydra重付,不需要等到程序的運行期間,因此s1屬于編譯期常量凫乖。而對s2來說确垫,雖然也被聲明為final類型,并且在聲明時就已經(jīng)初始化帽芽,但使用的不是常量表達式删掀,因此不屬于編譯期常量,這一類型的常量被稱為運行時常量导街。再看一下編譯后的字節(jié)碼文件中的常量池區(qū)域:

image

可以看到常量池中只有一個String類型的常量hello Hydra披泪,而s2對應(yīng)的字符串常量則不在此區(qū)域。對編譯器來說搬瑰,運行時常量在編譯期間無法進行折疊付呕,編譯器只會對嘗試修改它的操作進行報錯處理计福。

另外值得一提的是,編譯期常量與運行時常量的另一個不同就是是否需要對類進行初始化徽职,下面通過兩個例子進行對比:

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;
}

運行上面的代碼象颖,輸出:

init class
1

如果對上面進行修改姆钉,對變量a添加final進行修飾:

public static final int a=1;

再次執(zhí)行上面的代碼说订,會輸出:

1

可以看到在添加了final修飾后,兩次運行的結(jié)果是不同的潮瓶,這是因為在添加final后陶冷,變量a成為了編譯期常量,不會導致類的初始化毯辅。另外埂伦,在聲明編譯器常量時,final關(guān)鍵字是必要的思恐,而static關(guān)鍵字是非必要的沾谜,上面加static修飾只是為了驗證類是否被初始化過。

我們再看幾個例子來加深對final關(guā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

代碼中字符串h1h2都使用常量賦值基跑,區(qū)別在于是否使用了final進行修飾,對比編譯后的代碼描焰,s1進行了折疊而s2沒有媳否,可以印證上面的理論,final修飾的字符串變量屬于編譯期常量荆秦。

image

再看一段代碼篱竭,執(zhí)行下面的程序,結(jié)果會返回什么呢步绸?

public static void main(String[] args) {
    String h ="hello";
    final String h2 = h;
    String s = h2 + "Hydra";
    System.out.println(s=="helloHydra");
}

答案是false掺逼,因為雖然這里字符串h2final修飾,但是初始化時沒有使用編譯期常量靡努,因此它也不是編譯期常量坪圾。

在上面的一些例子中晓折,在執(zhí)行常量折疊的過程中都遵循了使用常量表達式進行初始化這一原則惑朦,這里可能有的同學還會有疑問,到底什么樣才能算得上是常量表達式呢漓概?在Oracle官網(wǎng)的文檔中漾月,列舉了很多種情況,下面對常見的情況進行列舉(除了下面這些之外官方文檔上還列舉了不少情況胃珍,如果有興趣的話梁肿,可以自己查看):

  • 基本類型和String類型的字面量
  • 基本類型和String類型的強制類型轉(zhuǎn)換
  • 使用+-!等一元運算符(不包括++--)進行計算
  • 使用加減運算符+蜓陌、-,乘除運算符*吩蔑、 / 钮热、% 進行計算
  • 使用移位運算符 >><<烛芬、 >>>進行位移操作
  • ……

字面量(literals)是用于表達源代碼中一個固定值的表示法隧期,在Java中創(chuàng)建一個對象時需要使用new關(guān)鍵字,但是給一個基本類型變量賦值時不需要使用new關(guān)鍵字赘娄,這種方式就可以被稱為字面量仆潮。Java中字面量主要包括了以下類型的字面量:

//整數(shù)型字面量:
long l=1L;
int i=1;

//浮點類型字面量:
float f=11.1f;
double d=11.1;

//字符和字符串類型字面量:
char c='h';
String s="Hydra";

//布爾類型字面量:
boolean b=true;

當我們在代碼中定義并初始化一個字符串對象后,程序會在常量池(constant pool)中緩存該字符串的字面量遣臼,如果后面的代碼再次用到這個字符串的字面量性置,會直接使用常量池中的字符串字面量。

除此之外揍堰,還有一類比較特殊的null類型字面量鹏浅,這個類型的字面量只有一個就是null,這個字面量可以賦值給任意引用類型的變量个榕,表示這個引用類型變量中保存的地址為空篡石,也就是還沒有指向任何有效的對象。

那么西采,如果不是使用的常量表達式進行初始化凰萨,在變量的初始化過程中引入了其他變量(且沒有被final修飾)的話,編譯器會怎樣進行處理呢械馆?我們下面再看一個例子:

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

為什么會出現(xiàn)不同的結(jié)果?在Java中霹崎,String類型在使用==進行比較時珊搀,是判斷的引用是否指向堆內(nèi)存中的同一塊地址,出現(xiàn)上面的結(jié)果那么說明指向的不是內(nèi)存中的同一塊地址尾菇。

通過之前的分析境析,我們知道s3會進行常量折疊,引用的是常量池中的ab派诬,所以相等劳淆。而字符串s2在進行拼接時,表達式中引用了其他對象默赂,不屬于編譯期常量沛鸵,因此不能進行折疊。

那么,在沒有常量折疊的情況下曲掰,為什么最后返回的是false呢疾捍?我們看一下這種情況下,編譯器是如何實現(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對字節(jié)碼文件進行反編譯乱豆,可以看到在這一過程中,編譯器同樣會進行優(yōu)化:

image

可以看到吊趾,雖然我們在代碼中沒有顯示的調(diào)用StringBuilder咙鞍,但是在字符串拼接的場景下,Java編譯器會自動進行優(yōu)化趾徽,新建一個StringBuilder對象续滋,然后調(diào)用append方法進行字符串的拼接。而在最后孵奶,調(diào)用了StringBuildertoString方法疲酌,生成了一個新的字符串對象,而不是引用的常量池中的常量了袁。這樣朗恳,也就能解釋為什么在上面的例子中,s2=="ab"會返回false了载绿。

本文代碼基于Java 1.8.0_261-b12 版本測試

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粥诫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子崭庸,更是在濱河造成了極大的恐慌怀浆,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怕享,死亡現(xiàn)場離奇詭異执赡,居然都是意外死亡,警方通過查閱死者的電腦和手機函筋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門沙合,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人跌帐,你說我怎么就攤上這事首懈。” “怎么了谨敛?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵究履,是天一觀的道長。 經(jīng)常有香客問我佣盒,道長挎袜,這世上最難降的妖魔是什么顽聂? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任肥惭,我火速辦了婚禮盯仪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蜜葱。我一直安慰自己全景,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布牵囤。 她就那樣靜靜地躺著爸黄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪揭鳞。 梳的紋絲不亂的頭發(fā)上炕贵,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音野崇,去河邊找鬼称开。 笑死,一個胖子當著我的面吹牛乓梨,可吹牛的內(nèi)容都是我干的鳖轰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼扶镀,長吁一口氣:“原來是場噩夢啊……” “哼蕴侣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起臭觉,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤昆雀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蝠筑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忆肾,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年菱肖,在試婚紗的時候發(fā)現(xiàn)自己被綠了客冈。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡稳强,死狀恐怖场仲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情退疫,我是刑警寧澤渠缕,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站褒繁,受9級特大地震影響亦鳞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一燕差、第九天 我趴在偏房一處隱蔽的房頂上張望遭笋。 院中可真熱鬧,春花似錦徒探、人聲如沸瓦呼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽央串。三九已至,卻和暖如春碗啄,著一層夾襖步出監(jiān)牢的瞬間质和,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工稚字, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留侦另,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓尉共,卻偏偏與公主長得像褒傅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子袄友,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容