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