why哥被阿里一道基礎面試題給幹懵了,一氣之下寫出萬字長文。

這是why的第 65 篇原創文章html

荒腔走板

你們好,我是 why,歡迎來到我連續周更優質原創文章的第 65 篇。老規矩,先荒腔走板聊聊技術以外的東西。java

上面這圖是去年的成都馬拉松賽道上,攝影師抓拍的我。哎,真是陽光向上的 95 後帥小夥啊。程序員

今年因爲疫情緣由,上半年的馬拉松比賽所有停擺了。今年可能也沒有機會再跑一次馬拉松了。只有回味一下去年的成都馬拉松了。面試

去年成都馬拉松我跑的是半程,只有 21 千米,女友也報名跑了一個 5 千米的歡樂跑,因此前 5 千米都是陪着她邊跑邊玩。編程

過了 10 千米後,賽道兩邊的觀衆愈來愈多,成都的叔叔阿姨們特別的熱情。老遠看到我跑過來了,就用四川話大聲的喊:帥哥,加油。segmentfault

還有不少老年人,手上拿着個小型國旗,在那裏手舞足蹈的揮舞着。數組

固然還有不少成羣結隊的小朋友,伸長了手臂,極力張開着五指。那是他們要和你擊掌的意思。oracle

每擊一次,跑過以後都能聽到小朋友那特有的一連串的笑聲。他們收穫了歡樂,而我收穫了力量。jvm

有一個轉彎的地方,路邊站着的男女老幼都伸長着手臂,張開着五指,延綿幾十米,每一個人嘴裏喊着鼓勁的話。編程語言

我放慢腳步,一個個的輕輕擊掌過去。這個時候耳機裏面傳來的是我循環播放的成都宣傳曲《I love this city》。

我不知道應該怎樣去描述那種氛圍帶給個人激勵和感動,感受本身就是奔跑在星光大道上,我很懷戀。

每跑完一次馬拉松,都能帶給我爆棚的正能量。

固然了,成都馬拉松的官方補給我也是吹爆的。可是給我印象深入的是大概在 16 千米的地方,有一處私人補給站,我竟然在這裏喝了到幾口烏蘇啤酒,吃了幾口豆花,幾根涼麪,幾塊冒烤鴨。逗留了大概 5 分鐘的樣子。

哎呀,那感受,難以忘懷,簡直是巴適的板。

好了,說迴文章。

阿里面試題

阿里巴巴出品的《碼出高效 Java 開發手冊》你知道吧?

前段時間我發現書的最後還有兩道 Java 基礎的面試題。其中有一道,很是的基礎,能夠說是入門級的題,可是都把我幹懵了。

竟然經過眼神編譯,看不出輸出結果是啥。

最後猜了個答案,結果還錯了。

這篇文章就帶着你們一塊兒看看這題,分析分析他背後的故事。

首先看題:

public class SwitchTest {  
    public static void main(String[] args) {  
      //當default在中間時,且看輸出是什麼?  
        int a = 1;  
        switch (a) {  
            case 2:  
                System.out.println("print 2");  
            case 1:  
                System.out.println("print 1");  
            default:  
                System.out.println("first default print");  
            case 3:  
                System.out.println("print 3");  
        }  
        
      //當switch括號內的變量爲String類型的外部參數時,且看輸出是什麼?  
        String param = null;  
        switch (param) {  
            case "param":  
                System.out.println("print param");  
                break;  
            case "String":  
                System.out.println("print String");  
                break;  
            case "null":  
                System.out.println("print null");  
                break;  
            default:  
                System.out.println("second default print");  
        }  
    }  
}

這題主要是考的 switch 控制語句,你能經過眼神編譯,在內心輸出運行結果嗎?

兩個考點

先看看答案:

怎麼樣,這個答案是否是和你本身給出來的答案一致呢?

反正我以前是被它那個 default 寫在中間的操做給迷惑了。

我尋思這玩意還有這種操做?能這樣寫嗎?

至於下面那個空指針,問題不大,一眼看出問題。

因此在我看來,這題一共兩個考點:

  • 前一個 switch 考的是其流程控制語言。
  • 後一個 switch 考的是其底層技術實現。

咱們一個個剝絲抽繭,扒光示衆的說。一塊兒把這個 switch 一頓爆學。

switch 執行流程

先看看考流程控制語句的:

這個程序的迷惑點在於第 5 行的註釋,致使我主要關注這個 default 的位置了,忽略了每一個 case 並無 break。

沒有 break 致使這個程序的輸出結果是這樣的:

那麼 switch 是怎麼控制流程的呢?

帶着這個問題咱們去權威資料裏面尋找答案。

什麼權威資料呢?

https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.11

怎麼樣?

The Java® Language Specification,《Java 語言規範》,你就告訴我權不權威?

打開我上面給的連接,在這個頁面那麼輕輕的一搜:

這就是咱們要找的東西。

點擊過去以後,在這個頁面裏面的信息量很是大。我一會都會講到。

如今咱們先關注執行流程這塊:

看到這麼多英語,不要慌,why 哥這種暖男做者,確定是給你翻譯的巴巴適適的。可是建議你們也看看英文原文,有的時候翻譯出來的可能就差點意思。

接下來我就給你們翻譯一下官方的話:

來,第一句:

當 switch 語句執行的時候,首先須要計算表達式。

等等,表達式(Expression)是什麼?

表達式就是 switch 後面的括號裏面的東西。好比說,這個東西能夠是一個方法。

那麼若是這個表達式的計算結果是 null,那麼就拋出空指針異常。這個 switch 語句也就算完事了。

另外,若是這個表達式的結果是一個引用類型,那麼還須要進行一個拆箱的處理。

好比就像這樣式兒的:

test() 方法就是表達式,返回的是包裝類型 Integer,而後 switch 會作拆箱處理。

這個場景下 test 方法返回了 null,因此會拋出空指針異常。

接着往下翻譯:

若是表達式的計算或者隨後的拆箱操做因爲某些緣由忽然完成,那麼這個 switch 語句也就完成了。

忽然完成,小樣,說的還挺隱晦的。我以爲這裏就是在說表達式裏面拋出了異常,那麼 switch 語句也就不會繼續執行了。

就像這樣式兒的:

接下來就是流程了:

Otherwise,就是不然的意思。帶入上下文也就是說前面的表達式是正常計算出來了一個東西了。

那麼就拿着計算出來的這個東西(表達式的值)和每個 case 裏面的常量來對比,會出現如下的狀況:

  • 若是表達式的值和其中一個 case 語句中的常量相等了,那麼咱們就說 case 語句匹配上了。switch 代碼塊中匹配的 case 語句以後的全部語句 (若是有)就按照順序執行。若是全部語句都正常完成,或者在匹配的 case 語句以後沒有語句,那麼整個 switch 代碼塊就將正常完成。
  • 若是沒有和表達式匹配的 case 語句,可是有一個 default 語句,那麼 switch 代碼塊中 default 語句後面的全部語句(若是有)將按順序執行。若是全部語句都正常完成,或者若是 default 標籤以後沒有語句了,則整個 switch 代碼塊就將正常完成。
  • 若是既沒有 case 語句和表達式的值匹配上,也沒有 default 語句,那就沒有什麼搞的了,switch 語句執行了個寂寞,也算是正常完成。

其實到這裏,上面的狀況一不就是阿里巴巴 Java 開發手冊的面試題的場景嗎?

你看着代碼,再看着翻譯,仔細的品一品。

爲何那道面試題的輸出結果是這樣的:

沒有爲何,Java 語言規範裏面就是這樣規定的,按照規定執行就完事了。

除了上面這三種流程,官網上還接着寫了三句話:

若是 switch 語句塊裏面包含任何的表示或者意外致使當即完成的語句,則按以下方式處理:

我先說一下我理解的官方文檔中說的:「any statement immediately ... completes abruptly」。

表示當即完成的語句就是每一個 case 裏面的 break、return。

意外致使忽然完成的語句就是在 switch 語句塊裏面任何會拋出異常的代碼。

若是出現了這兩種狀況,switch 語句塊怎麼處理呢?

若是語句的執行因爲 break 語句而完成,則不會採起進一步的操做(進一步操做是指若是沒有 break 代碼,則將繼續執行後續語句),switch 語句塊將正常完成。

若是語句的執行因爲任何其餘緣由忽然完成(好比拋出異常),switch 語句塊也會因相同的緣由而立馬完成。

上面就是 switch 語句的執行流程。因此你還別以爲 switch 語句就必需要個 break,別人的設計就是如此,看場景的。

好比看官方給出的兩個示例代碼:

這是不帶 break 的。需求就要求這樣輸出,你整個 break 幹啥。

再看另一個帶 break 的:

實現的又是另一個需求了。

因此,看場景。

另外,我以爲官網上的這個例子給的很差。最後少了一個 default 語句。看看阿里 Java 開發手冊上怎麼說的:

這個地方見仁見智吧。

底層技術實現

第二個考點是底層技術實現。

也就下面這坨代碼:

首先通過前面的一個小節,你知道爲何運行結果是拋出空指針異常了不?

前面講了哈,官方文檔裏面有這樣的一句話:

規定如此。

因此,這小節的答案是這樣的嗎?確定不是的,咱們多想一步:

爲何這樣規定呢?

這纔是這小節想要帶你們尋找的東西。

首先你得知道 switch 支持 String 是 Java 的一顆語法糖。既然是語法糖, 咱們就看看它的 class 文件:

從 class 文件中,咱們嚐到了這顆語法糖的味道。原來其實是有兩個 switch 操做的。

switch 支持 String 類型的緣由是先取的 String 的 hashCode 進行 case 匹配,而後在每一個 case 裏面給 var3 這個變量賦值。而後再對 var3 進行一次 switch 操做。

因此,上圖中標記的 15 行,若是 String 是 null,那麼對 null 取 hashCode ,那可不得拋出空指針異常嗎?

因此,你看《Java開發手冊》裏面的這個建議:

明白爲何這樣寫了吧?

因此,這小節的答案是這樣的嗎?確定不是的,咱們再多想一步呢:

爲何要非得把 String 取 hashCode 才進行 switch/case 操做呢?

從 class 文件中咱們已經看不出什麼有價值的東西了。只能在往下走。

class 再往下走就到哪裏了?

對了,須要看看字節碼了。

經過 javap 得到字節碼文件:

這個字節碼很長,你們本身編譯後去看一下,我就不所有截取,浪費篇幅了。

在這個字節碼裏面,就算你什麼都不太明白。可是隻要你稍微注意一點點,你應該會注意到其中的這兩個地方:

結合着 class 文件看:

奇怪了,一樣的 switch 語言,卻對應兩個指令:lookupswitch 和 tableswitch。

因此這兩個指令確定是關鍵突破點。

咱們去哪裏找這個兩個指令的信息呢?

確定是得找權威資料的:

怎麼樣?

The Java® Virtual Machine Specification,Java 虛擬機規範,你就大聲的告訴我穩不穩?

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.10

在上面的連接中,咱們輕輕的那麼一搜:

發現這兩個指令,在 Compiling Switches 這一小節中是挨在一塊兒的。

找到這裏了,你就找到正確答案的門了。我帶領你們看一下我經過這個門,看到的門後面的世界。

首先仍是給你們帶着我本身的理解,翻譯一下虛擬機規範裏面是怎麼介紹這兩個指令的:

switch 語句的編譯使用的是 tableswitch 和 lookupswitch 這兩個指令。

咱們先說說 tableswitch 是幹啥的。

當 switch 裏面的 case 能夠用偏移量進行有效表示的時候,咱們就用 tableswitch 指令。若是 switch 語句的表達式計算出來的值不在這個偏移量的有效範圍內,那麼就進入 default 語句。

看不太明白對不對?

不要緊,我第一次看的時候也不太明白。別急,咱們看看官方示例:

由於咱們 case 的條件是 0、一、2 這三個挨在一塊兒的數據,挨在一塊兒就是 near 。因此這個方法就叫作 chooseNear 。

而這個 0、一、2 就是三個連在一塊兒的數字,因此咱們能夠用偏移量直接找到其對應的下一個須要跳轉的地址。

這個就有點相似於數組,直接經過索引下標就能定位到數據。而下標,是一串連續的數字。

這個場景下,咱們就能夠用 tableswitch。

接着往下看:

當 switch 語句裏面 case 的值比較「稀疏」(sparse)的時候,用 tableswitch 指令的話空間利用率就會很低下。因而咱們就用 lookupswitch 指令來代替 tableswitch。

你注意官網上用的這個詞:sparse。

沒想到吧,學技術的時候還能學個英語四級單詞。

稀疏。翻譯過來了,仍是讀不懂是否是,沒有關係。我給你搞個例子:

左邊是 java 文件,裏面的 case 只有 0、二、4。

右邊是字節碼文件, tableswitch 裏面有0、一、二、三、4。

對應的 class 文件是這樣的:

嘿,你說怎麼着?莫名其妙多了個 1 和 3 的 case 。你說神奇不神奇?

這是在幹嗎?這不就是在填位置嘛。

填位置的目的是什麼?不就是爲了保證 java 文件裏面的 case 對應的值恰好能和偏移量對上嗎?

假設這個時候 switch 表達式的值是 2,我直接根據偏移量 2 ,就能夠取到 2 對應的接下來須要執行的地方 47,而後接着執行輸出語句了:

假設這個時候 switch 表達式的值是 3,我直接根據偏移量 3,就能夠取到 3 對應的接下來須要執行的地方 69,而後接着執行 default 語句了:

因此,0,1,2 不叫稀疏,0,2,4 也不叫稀疏。

它們都不 sparse ,缺一點點的狀況下,咱們能夠補位。

因此如今你理解官網上的這句話了嗎:

當 switch 語句裏面 case 的值比較「稀疏」(sparse)的時候,用 tableswitch 指令的話空間利用率就會很低下。

比較稀疏的時候,假設三個 case 分別是 100,200,300。你不可能把 100 到 300 之間的數,除了 200 都補上吧?

那玩意補上了以後 case 得膨脹成什麼樣子?

空間佔的多了,可是實際要用的就 3 個值,因此空間利用率低下。

那 tableswitch 指令不讓用了怎麼辦呢?

別急,官方說能夠用 lookupswitch 指令。

lookupswitch 指令拿着 switch 表達式計算出來的 int 值和一個表中偏移量進行配對(pairs)。

配對的時候,若是表裏面一個 key 值與表達式的值配上了,就能夠在這個 key 值關聯的下一執行語句處繼續執行。

若是表裏面沒有匹配上的鍵,則在 default 處繼續執行。

你看明白了嗎?迷迷糊糊的對不對?

什麼玩意就出來一個表呢?

沒事,別急,官方給了個例子:

此次的例子叫作 chooseFar 。由於 case 裏面的值不是挨着的,0 到 100 之間隔得仍是有點距離。

我不能像 tableswitch 似的,拿着 100 而後去找偏移量爲 100 的位置吧。這裏就三個數,根本就找不到 100 。

只能怎麼辦?

就拿着我傳進來的 100 一個個的去和 case 裏面的值比了,這就叫 pairs。

其實官網上的這個例子沒有給好,你看我給你一個例子:

你看左邊的 java 代碼,裏面的 case 是亂序的,到字節碼文件裏面後就排好序了。

而官方文檔裏面說的這個「table」:

就是排好序的這個:

爲何要排序呢?

答案就在虛擬機規範裏面:

排序以後的查找比線性查找快。這個沒啥說的吧。它這裏雖然沒有說,但其實它用的是二分查找,時間複雜度爲O(log n)。

哦,對了。tableswitch 因爲是直接根據偏移量定位,因此時間複雜度是 O(1)。

好了,到這裏我就把 tableswitch 和 lookupswitch 這兩個指令講完了。

我不知道你在看的時候有沒有產生什麼疑問,反正我看到這個地方的時候我就在想:

虛擬機規範裏面就說了個 sparse,那何時是稀疏,何時是不稀疏呢?

說實話,做爲程序員,我對「稀疏」這個詞仍是很敏感的,特別是前面再加上毛髮兩個字的時候。

不知道爲何說到「稀疏」,我就想起了謝廣坤。廣坤叔你知道吧,這才叫「稀疏」:

怎麼定義稀疏

因此,在 switch 裏面,咱們怎麼定義稀疏呢?

文檔中沒有寫。

文檔裏沒有寫的,都在源碼裏面。

因而我搞了個 openJDK,我倒要看看源碼裏面到底什麼是 TMD 稀疏。

通過一番探索,找到了這個方法:

com.sun.tools.javac.jvm.Gen#visitSwitch

這裏我不作源碼解讀,我只是想單純的知道源碼裏面到底什麼 TMD 是 TMD 稀疏。

因此帶你們直接看這個地方:

這裏有個三目表達式。若是爲真則使用 tableswitch ,爲假則使用 lookupswitch。

咱們先拿着這個不稀疏的,加上斷點調戲一番,呸,調試一番:

斷點時候時候各個參數以下:

標號爲 ① 的地方是表明咱們確實調試的是預期的程序。

標號爲 ② 的地方咱們帶入到上面的表達式中,能夠求得最終值:

hi 是 case 裏面的表達式對應的最大值,也就是 2。

lo 是 case 裏面的表達式對應的最小值,也就是 0。

nlabels 表明的是 case 的個數,也就是 3。

因此帶入到上面的代碼中,最終算出來的值 16<=18,成立,使用 tablewitch。

這就叫不稀疏。

假設咱們把最後一個 case 改成 5:

Debug 時各個參數變成了這樣:

最終算出來的值 19<=18,不知足,使用 lookupswitch 。

這叫作稀疏。

因此如今咱們知道了到底什麼是 TMD 稀疏。

在源碼裏面有個公式能夠知道是否是稀疏的,從而知道使用什麼指令。

寫到這裏我以爲其實我應該能夠住手了。

可是我還在《Java 虛擬機規範》的文檔裏面挖到了一句話。我以爲得講一下。

switch表達式支持的類型

在《Java 虛擬機規範》文檔中的這一部分,有這樣的一句話:

就看第一句我圈起來的話。後面的描述都是圍繞着這句話在展開描述。

Java 虛擬機的 tableswitch 和 lookupswitch 指令,只支持 int 類型。

好,那我如今來問你:switch 語句的表達式能夠是哪些類型的值?注意我說的是表達式。

這個答案在《Java 語言規範》裏面也寫着的:

你看,8 種基本類型已經支持了char、byte、short、int 這4 種,而這 4 種都是能夠轉化爲 int 類型的。

而剩下的 4 種:double、float、long、boolean 不支持。

爲何?

你就想,你就結合我前面講的內容,把你的小腦袋子動起來,爲何這 4 種不支持?

由於 double、float 都是浮點類型的,tableswitch 和 lookupswitch 指令操做不了。

由於 long 類型 64 位了,而tableswitch 和 lookupswitch 指令只能操做 32 位的 int 。這兩個指令對於 long 是搞不動的。

而至於 boolean 類型,還須要我說嘛?

你拿着 boolean 類型放到 switch 表達式裏面去,你不以爲害臊嗎?

你就不能寫個 if(boolean) 啥的?

而後你又發動你的小腦袋子想:對於 Character、Byte、Short、Integer 這 4 個包裝類型是怎麼支持的呢?

上個圖,左上是 java 文件,右上是 jad 文件,下面是字節碼:

拆了個箱,實際仍是用的 int 類型,這個不須要我細講了吧?

因而你接着想對於 String 類型是怎麼支持的呢?

它會先轉 hashCode。hashCode 確定是稀疏的,因此用 lookupswitch。

而後在用 var3 這個變量去作一次 switch,通過轉化後 var3 必定不是稀疏的,因此用 tableswitch:

你再多想一步,由於是用的 String 類型的 hashcode,那若是出現了哈希衝突怎麼辦?

看一下這個例子:

衝突了就再配一個 if-else 。

不用多說了吧。

最後,你再想,這個枚舉又是怎麼支持的呢?

好比下面這個例子,看字節碼,只看到了使用了 tableswitch:

咱們再看一下 class 文件,javap 編譯以後,變成了這樣:

它們分別長這樣的:

上面的 SwitchEnumTest.class 文件看不出來什麼道道。

可是下面的 SwitchEnumTest$1.class 文件裏面仍是有點東西的。

能夠看到靜態代碼塊裏面有個數組,數組裏面的參數是枚舉的類型,而後調用了枚舉的 ordinal 方法。這個方法的返回值是枚舉的下標位置。

在 class 文件裏面獲取的信息有限,須要祭出 jad 文件來瞅一眼來:

上面就是 java 文件對應的 jad 文件。

標號爲 ① 的地方是咱們傳入的 switch 裏面的表達式,線程狀態枚舉中的 RUNNABLE。

標號爲 ② 的地方是給 int 數值中的位置賦值爲 2。那麼是哪一個位置呢?

RUNNABLE 在線程狀態枚舉中的下標位置,以下所示,下標位置是1:

編號爲 ③ 的地方是把 int 數值中下標爲 1 的元素取出來?

咱們前面剛剛放進去的。取出來是 2。

因而走到編號爲 ④ 的邏輯中去。執行最終的輸出語句。

因此寫到這裏,我想我更加能明白著名程序員沃·滋基索德的一句話:

相對於 String 類型而言,枚舉簡直天生就支持 Switch 操做。

奇怪的知識點

再送給你一個我在寫這篇文章的時候學到的一個奇怪的知識點。

咱們知道 switch 的表達式和 case 裏面都是不支持 null 的。

你有沒有想過一個問題。case 裏面爲何不支持 null?若是表達式爲 null ,咱們就拿着 null 去 case 裏面匹配,這樣理論上作也是能夠作的。

好吧,應該也沒有人想這個問題。固然,除了一些奇奇怪怪的面試官。

這個問題我在《Java 語言規範》裏面找到了答案:

the designers of the Java programming language。

個人媽呀,這是啥啊。

Java 編程語言設計者,這是賞飯吃的祖師爺啊!

《Java 語言規範》裏面說:根據 Java 編程語言設計者的判斷,拋出空指針這樣作比靜默地跳過整個 switch 語句或選擇在 default 標籤(若是有)裏面繼續執行語句要好。

別問,問就是祖師爺以爲這樣寫就是好的。

一個基本上用不到的知識點送給你們,沒必要客氣:

最後說一句(求關注)

這篇文章裏面仍是不少須要翻譯的地方。我發現有不少的程序猿比較懼怕英語。

以前還有人誇我英語翻譯的好:

其實我大學的時候英語四級考了 4 次,最後一次才壓線過的。

那爲何如今看英文文檔基本上沒有什麼障礙呢?

其實這個問題真的很好解決的。

你找一個英語六級 572 分,考研英語一考了 89 分的女友,她會督促你學英語的。

才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在留言區提出來,我對其加以修改。 感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。

還有,重要的事情說三遍:歡迎關注我呀。歡迎關注我呀。歡迎關注我呀。

相關文章
相關標籤/搜索