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

這是why的第 65 篇原創文章

荒腔走板

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

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

今年由於疫情原因,上半年的馬拉松比賽全部停擺了。今年可能也沒有機會再跑一次馬拉鬆了。只有回味一下去年的成都馬拉鬆了。

去年成都馬拉松我跑的是半程,只有 21 公里,女朋友也報名跑了一個 5 公里的歡樂跑,所以前 5 公里都是陪着她邊跑邊玩。

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

還有很多老年人,手上拿着個小型國旗,在那裡手舞足蹈的揮舞着。

當然還有很多三五成群的小朋友,伸長了手臂,極力張開着五指。那是他們要和你擊掌的意思。

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

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

我放慢腳步,一個個的輕輕擊掌過去。這個時候耳機裏面傳來的是我循環播放的成都宣傳曲《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 是怎麼控制流程的呢?

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

什麼權威資料呢?

//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 虛擬機規範,你就大聲的告訴我穩不穩?

//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、1、2 這三個挨在一起的數據,挨在一起就是 near 。所以這個方法就叫做 chooseNear 。

而這個 0、1、2 就是三個連在一起的數字,所以我們可以用偏移量直接找到其對應的下一個需要跳轉的地址。

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

這個場景下,我們就可以用 tableswitch。

接着往下看:

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

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

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

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

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

右邊是位元組碼文件, tableswitch 裏面有0、1、2、3、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,一個被代碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。

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