這是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 一頓爆學。
先看看考流程控制語句的:
這個程序的迷惑點在於第 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 裏面的常量來對比,會出現如下的狀況:
其實到這裏,上面的狀況一不就是阿里巴巴 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 虛擬機規範》的文檔裏面挖到了一句話。我以爲得講一下。
在《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,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。
還有,重要的事情說三遍:歡迎關注我呀。歡迎關注我呀。歡迎關注我呀。