你好,我是 阿遠(yuǎn)。
一般面試我都會(huì)問(wèn)一兩道很基礎(chǔ)的題目,來(lái)考察候選人的“地基”是否扎實(shí),有些是操作系統(tǒng)層面的,有些是 JAVA 語(yǔ)言方面的,還有些…
最近我都拿一道 Java 語(yǔ)言基礎(chǔ)題來(lái)考察候選人:
不用反射,能否實(shí)現(xiàn)一個(gè)方法,調(diào)換兩個(gè) String 對(duì)象的實(shí)際值?
String yesA = "a";
String yesB = "b";
//能否實(shí)現(xiàn)這個(gè) swap 方法
// 讓yesA=b,yesB=a?
swap(yesA, yesB);
別小看這道題,其實(shí)可以考察好幾個(gè)點(diǎn):
1.明確 yesA 和 yesB 是啥
2.Java 只有值傳遞
3.String 是不可變類
4.字符串常量池
5.intern 的理解
6.JVM內(nèi)存的劃分與改變
基于上面這幾個(gè)點(diǎn),其實(shí)還能發(fā)散出很多面試題,不過(guò)今天咱們這篇文章就不發(fā)散了,好好消化上面這幾個(gè)點(diǎn)就可以了。
我們需要明確答案:實(shí)現(xiàn)不了這個(gè)方法。
按照題意,我相信你很容易能寫出以下的 swap 方法實(shí)現(xiàn):
void swap(String yesA, String yesB){
String temp = yesA;
yesA = yesB;
yesB = temp;
}
首先,我們要知道 String yesA = "a"; 這行代碼返回的 yesA 代表的是一個(gè)引用,這個(gè)引用指向堆里面的對(duì)象 a。
也就是說(shuō)變量 yesA 存儲(chǔ)的只是一個(gè)引用,通過(guò)它能找到 a 這個(gè)對(duì)象,所以表現(xiàn)出來(lái)好像 yesA 就是 a,實(shí)際你可以理解 yesA 存儲(chǔ)是一個(gè)“地址”,Java 通過(guò)這個(gè)地址就找到對(duì)象 a。
因此,我們知道了, yesA 存儲(chǔ)的值不是 a,是引用(同理,yesB也一樣)。
然后,我們都聽(tīng)過(guò) Java 中只有值傳遞,也就是調(diào)用方法的時(shí)候 Java 會(huì)把變量 yesA 的值傳遞到方法上定義的 yesA(同理 yesB 也是一樣),只是值傳遞。
根據(jù)上面我們已經(jīng)知道 yesA 存儲(chǔ)的是引用,所以我們得知,swap方法 里面的 yesA 和 yesB 拿到的是引用。
然后調(diào)用了 swap 方法,調(diào)換了 yesA 和 yesB 的值(也就是它的引用)
請(qǐng)問(wèn),swap 里的跟我外面的 yesA 和 yesB 有關(guān)系嗎?顯然,沒(méi)有關(guān)系。
因此最終外面的 yesA 指向的還是 a,yesB 指向的還是 b。
不信的話,我們看下代碼執(zhí)行的結(jié)果:
現(xiàn)在,我們明確了,Java 只有值傳遞。
看到這,可能會(huì)有同學(xué)疑惑,那 int 呢,int 不是對(duì)象呀,沒(méi)引用啊,其實(shí)一樣的,記住Java 只有值傳遞。
我們跑一下就知道了:
很顯然, int 也無(wú)法交換成功,道理是一樣的。
外面的 yesA 和 yesB,存儲(chǔ)的值是 1 和 2(這里不是引用了,堆里也沒(méi)有對(duì)象,棧上直接分配值)。
調(diào)用 swap 時(shí)候,傳遞的值是 1 和 2,你可以理解為拷貝了一個(gè)副本過(guò)去。
所以 swap 里的 yesA 和 yesB 實(shí)際上是副本,它的值也是 1 和 2,然后副本之間進(jìn)行了交換,那跟正主有關(guān)系嗎?
顯然沒(méi)有。
像科幻電影里面有克隆人,克隆人死了,正主會(huì)死嗎?
不會(huì)。
記住,Java 只有值傳遞。
再回到這個(gè)面試題,你需要知道 String 是不可變類。
那什么是不可變類呢?
我在之前的文章說(shuō)過(guò),這邊我引用一下:
不可變類指的是無(wú)法修改對(duì)象的值,比如 String 就是典型的不可變類,當(dāng)你創(chuàng)建一個(gè) String 對(duì)象之后,這個(gè)對(duì)象就無(wú)法被修改。
因?yàn)闊o(wú)法被修改,所以像執(zhí)行s += “a”; 這樣的方法,其實(shí)返回的是一個(gè)新建的 String 對(duì)象,老的 s 指向的對(duì)象不會(huì)發(fā)生變化,只是 s 的引用指向了新的對(duì)象而已。
看下面這幅圖應(yīng)該就很清晰了:
如圖所示,每次其實(shí)都是新建了一個(gè)對(duì)象返回其引用,并不會(huì)修改以前的對(duì)象值,所以我們常說(shuō)不要在字符串拼接頻繁的場(chǎng)景不要使用 + 來(lái)拼接,因?yàn)檫@樣會(huì)頻繁的創(chuàng)建對(duì)象,影響性能。
而一般你說(shuō)出 String 是不可變類的時(shí)候,面試官一般都會(huì)追問(wèn):
不可變類有什么好處?
來(lái),我也為你準(zhǔn)備好答案了:
最主要的好處就是安全,因?yàn)橹獣赃@個(gè)對(duì)象不可能會(huì)被修改,在多線程環(huán)境下也是線程安全的(你想想看,你引用的對(duì)象是一個(gè)不可變的值,那么誰(shuí)都無(wú)法修改它,那它永遠(yuǎn)就是不變的,別的線程也休息動(dòng)它分毫,你可以放心大膽的用)。
然后,配合常量池可以節(jié)省內(nèi)存空間,且獲取效率也更高(如果常量池里面已經(jīng)有這個(gè)字符串對(duì)象了,就不需要新建,直接返回即可)。
所以這里就提到 字符串常量池了。
例如執(zhí)行了 String yesA = "a" 這行代碼,我們現(xiàn)在知道 yesA 是一個(gè)引用指向了堆中的對(duì)象 a,再具體點(diǎn)其實(shí)指向的是堆里面的字符串常量池里的對(duì)象 a。
如果字符串常量池已經(jīng)有了 a,那么直接返回其引用,如果沒(méi)有 a,則會(huì)創(chuàng)建 a 對(duì)象,然后返回其引用。
這種叫以字面量的形式創(chuàng)建字符串。
還有一種是直接 new String,例如:
String yesA = new String("a")
這種方式又不太一樣,首先這里出現(xiàn)了字面量 “a”,所以會(huì)判斷字符串常量池里面是否有 a,如果沒(méi)有 a 則創(chuàng)建一個(gè) a,然后會(huì)在堆內(nèi)存里面創(chuàng)建一個(gè)對(duì)象 a,返回堆內(nèi)存對(duì)象 a 的引用,也就是說(shuō)返回的不是字符串常量池里面的 a
我們從下面的實(shí)驗(yàn)就能驗(yàn)證上面的說(shuō)法,用字面量創(chuàng)建返回的引用都是一樣的,new String 則不一樣
至此,你應(yīng)該已經(jīng)清晰字面量創(chuàng)建字符串和new String創(chuàng)建字符串的區(qū)別了。
講到這,經(jīng)常還會(huì)伴隨一個(gè)面試題,也就是 intern
以下代碼你覺(jué)得輸出的值各是啥呢?你可以先思考一下
String yesA = "aaabbb";
String yesB = new String("aaa") + new String("bbb");
String yesC = yesB.intern();
System.out.println(yesA == yesB);
System.out.println(yesA == yesC);
好了,公布答案:
第一個(gè)輸出是 false 應(yīng)該沒(méi)什么疑義,一個(gè)是字符串常量的引用,一個(gè)是堆內(nèi)的(實(shí)際上還是有門道的,看下面)。
第二個(gè)輸出是 true 主要是因?yàn)檫@個(gè) intern 方法。
intern 方法的作用是,判斷下 yesB 引用指向的值在字符串常量里面是否有,如果沒(méi)有就在字符串常量池里面新建一個(gè) aaabbb 對(duì)象,返回其引用,如果有則直接返回引用。
在我們的例子里,首先通過(guò)字面量定義了 yesA ,因此當(dāng)定義 yesC 的時(shí)候,字符串常量池里面已經(jīng)有 aaabbb 對(duì)象(用equals()方法確定是否有對(duì)象),所以直接返回常量池里面的引用,因此 yesA == yesC
你以為這樣就結(jié)束了嗎?
我們把上面代碼的順序換一下:
String yesB = new String("aaa") + new String("bbb");
String yesC = yesB.intern();
String yesA = "aaabbb"; // 這里換了
System.out.println(yesA == yesB);
System.out.println(yesA == yesC);
把 yesA 的定義放到 yesC 之后,結(jié)果就變了:
是不是有點(diǎn)懵?奇了怪了,按照上面的邏輯不應(yīng)該啊。
實(shí)際上,我最初畫字符串常量池的時(shí)候,就將其畫在堆內(nèi),也一直說(shuō)字符串常量池在堆內(nèi),這是因?yàn)槲沂钦驹?JDK 1.8 的角度來(lái)說(shuō)事兒的。
在 JDK 1.6 的時(shí)候字符串常量池是放在永久代的,而 JDK 1.7 及之后就移到了堆中。
這區(qū)域的改變就導(dǎo)致了 intern 的返回值有變化了。
在這個(gè)認(rèn)知前提下,我們?cè)賮?lái)看修改順序后的代碼具體是如何執(zhí)行的:
1.String yesB = new String("aaa") + new String("bbb");
此時(shí),堆內(nèi)會(huì)新建一個(gè) aaabbb 對(duì)象(對(duì)于 aaa 和 bbb 的對(duì)象討論忽略),字符串常量池里不會(huì)創(chuàng)建,因?yàn)椴](méi)有出現(xiàn) aaabbb 這個(gè)字面量。
2.String yesC = yesB.intern();
此時(shí),會(huì)在字符串常量池內(nèi)部創(chuàng)建 aaabbb 對(duì)象?
關(guān)鍵點(diǎn)來(lái)了。
在 JDK 1.6 時(shí),字符串常量池是放置在永久代的,所以必須新建一個(gè)對(duì)象放在常量池中。
但 JDK 1.7 之后字符串常量池是放在堆內(nèi)的,而堆里已經(jīng)有了剛才 new 過(guò)的 aaabbb 對(duì)象,所以沒(méi)必要浪費(fèi)資源,不用再存儲(chǔ)一份對(duì)象,直接存儲(chǔ)堆中的引用即可,所以 yesC 這個(gè)常量存儲(chǔ)的引用和 yesB 一樣。
3.String yesA = "aaabbb";
同理,在 1.7 中 yesA 得到的引用與 yesC 和 yesB 一致,都指向堆內(nèi)的 aaabbb 對(duì)象。
4.最終的答案都是 true
現(xiàn)在我們知曉了,在 1.7 之后,如果堆內(nèi)已經(jīng)存在某個(gè)字符串對(duì)象的話,再調(diào)用 intern 此時(shí)不會(huì)在字符串常量池內(nèi)新建對(duì)象,而是直接保存這個(gè)引用然后返回。
你看這面試題坑不坑,你還得站在不同的 JDK 版本來(lái)回答,不然就是錯(cuò)的,但是面試官并不會(huì)提醒你版本的情況。
其實(shí)很多面試題都是這樣的,看似拋給你一個(gè)問(wèn)題,你好像能直接回答,如果你直接回答,那就錯(cuò)了,你需要先聲明一個(gè)前提,然后再回答,這樣才正確。
最后
你看,就這么一個(gè)小小的基礎(chǔ)題就可以引出這么多話題,還能延伸到 JVM 內(nèi)存的劃分等等。
這其實(shí)很考驗(yàn)基礎(chǔ),也能看出來(lái)一個(gè)人學(xué)習(xí)的知識(shí)是否串起來(lái),因?yàn)檫@些知識(shí)都是有關(guān)聯(lián)性的,給你一個(gè)點(diǎn),就能擴(kuò)散成面,這樣的知識(shí)才成體系。
歡迎關(guān)注我~






