https://mp.weixin.qq.com/s/LTT66W1Swhdir_2ecetBvA html
你們好,我是 why,歡迎來到我連續周更優質原創文章的第 65 篇。老規矩,先荒腔走板聊聊技術以外的東西。java
上面這圖是去年的成都馬拉松賽道上,攝影師抓拍的我。哎,真是陽光向上的 95 後帥小夥啊。
程序員
今年因爲疫情緣由,上半年的馬拉松比賽所有停擺了。從大二開始,我每一年至少報名跑一次馬拉松,今年可能也沒有機會在賽場上跑一次馬拉松了。只有回味一下去年的成都馬拉松了。web
去年成都馬拉松我跑的是半程,只有 21 千米,女友也報名跑了一個 5 千米的歡樂跑,因此前 5 千米都是陪着她邊跑邊玩。面試
過了 10 千米後,賽道兩邊的觀衆愈來愈多,成都的叔叔阿姨們特別的熱情。老遠看到我跑過來了,就用四川話大聲的喊:帥哥,加油。編程
還有不少老年人,手上拿着個小型國旗,在那裏手舞足蹈的揮舞着。數組
固然還有不少成羣結隊的小朋友,伸長了手臂,極力張開着五指。那是他們要和你擊掌的意思。微信
每擊一次,跑過以後都能聽到小朋友那特有的一連串的笑聲。他們收穫了歡樂,而我收穫了力量。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 一頓爆學。
先看看考流程控制語句的:
這個程序的迷惑點在於第 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 得膨脹成什麼樣子?
補的 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,裏面的 case 必定是一串連續的數字,不稀疏,因此用 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 的。
你有沒有想過一個問題。switch/case 裏面爲何不作成支持 null 的模式?
若是表達式爲 null ,咱們就拿着 null 去 case 裏面匹配,這樣理論上作也是能夠作的。
好吧,應該也沒有人想這個問題。固然,除了一些奇奇怪怪的面試官。
這個問題我在《Java 語言規範》裏面找到了答案:
the designers of the Java programming language。
個人媽呀,這是啥啊。
Java 編程語言設計者,這是賞飯吃的祖師爺啊!
《Java 語言規範》裏面說:根據 Java 編程語言設計者的判斷,拋出空指針這樣作比靜默地跳過整個 switch 語句或選擇在 default 標籤(若是有)裏面繼續執行語句要好。
別問,問就是祖師爺通過判斷後,以爲這樣寫就是好的。
這題就像我以前寫的這個文章同樣:《這道面試題我真不知道面試官想要的回答是什麼》。
請問:ConcurrentHashMap中的key爲何不能爲null?
別問,問就是 Doug Lea 不喜歡 null。設計之初就沒打算支持 null。
好了,又一個基本上用不到的知識點送給你們,沒必要客氣:
這篇文章裏面仍是不少須要翻譯的地方。我發現有不少的程序猿比較懼怕英語。
以前還有人誇我英語翻譯的好:
其實我大學的時候英語四級考了 4 次,最後一次才壓線過的。
那爲何如今看英文文檔基本上沒有什麼障礙呢?
其實這個問題真的很好解決的。
你找一個英語六級 572 分,考研英語一考了 89 分的女友,她會督促你學英語的。
哦,對了文章中提到的《阿里巴巴Java開發手冊》能夠在公衆號後臺回覆關鍵字【java】便可得到電子版。
好了,看到了這裏安排個「一鍵三連」(轉發、在看、點贊)吧,周更很累的,不要白嫖我,須要一點正反饋。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在留言區提出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。
還有,重要的事情說三遍:歡迎關注我呀。歡迎關注我呀。歡迎關注我呀。