Java 12將在兩個月後(2019/3/19)發佈,現已進入RDP1階段,確定加入8個JEP。其中對Java語法的改進是JEP 325: switch表達式。於是我迫不及待,提前感受一下更先進的語言特性。 ...
Java 12將在兩個月後(2019/3/19)發佈,現已進入RDP1階段,確定加入8個JEP。其中對Java語法的改進是JEP 325: switch表達式。於是我迫不及待,提前感受一下更先進的語言特性。
因為12沒有正式發佈,本文使用自己編譯的OpenJDK。嫌麻煩的話,也可以直接使用官方的ea版本。JEP325是預覽(preview)特性,編譯運行時需要添加--enable-preview
參數。
顧名思義,這個feature是對switch
動手腳的。包括兩個方面。
1. 簡化fall-through規則
下麵這樣的switch
代碼我們寫過幾萬遍了
switch (today) { case SATURDAY: case SUNDAY: System.out.println("I'm happy!"); break; case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: System.out.println("I'm sad..."); break; default: System.out.println("I'm confused."); }
這段代碼存在的問題是:
1. 內容不符合愛崗敬業的核心價值觀(敲黑板!重要!!)
2. 多個條件對應相同代碼時(比如MONDAY到FRIDAY),要重覆寫多個case
,冗餘且醜陋
3. 每一段代碼後面都要有break
,一旦忘記就會有編譯器檢測不到的邏輯錯誤
4. 變數作用域混亂
第四個問題可能長被忽略。case
或者default
後面是一連串的語句,而不是代碼塊(註意,它是沒有大括弧的)。這種情況下定義的局部變數,其作用域不是case
後的部分,而是整個switch
結構。因此,下麵的代碼無法通過編譯。
switch (today) { case MODAY: int x = 1; break; default: int x = 0; //Variable x is already defined in the scope }
編譯器看到的是在一個作用域中存在兩個x
,非常違背人類的直覺。
上面的四個問題,除了1,剩下的萬惡之源就是fall-through規則。即switch
結構在找到第一個匹配的case
條件後,會順序執行後面所有case
對應的代碼,無論是否判斷為真。這是40多年前C語言創造後來Java原樣照抄的經典語法,但在今天看起來就顯得很呆萌了,新的語言也幾乎都放棄了fall-through。
好在,儘管後知後覺,從12開始Java開發者也可以選擇更簡潔清晰的語法了。就像這樣
switch (today) { case SUNDAY, SATURDAY -> System.out.println("I'm happy!"); case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> System.out.println("I'm happy, too!!"); default -> System.out.println("I'm confused."); }
很容易看出語法的變化,這些變化也解決了上面的四個問題。歸納一下:
1. 程式內容積極向上,體現了新時代的奮鬥精神(敲黑板!重要!!)
2. 對應相同動作的多個case
合併為一行,代碼更簡潔
3. 條件和動作之間用->
連接,這時fall-through規則失效。匹配到的分支代碼執行完後直接跳出,不會繼續執行下麵的case
對應的代碼。也就是不需要再為每一個分支寫break
了。程式更簡潔清晰,也更符合人類的直覺。
需要註意,為了保持向後相容性,case
條件後依然可以使用:
,這時fall-through還是有效的,即不能省略原有的break
。而一個switch
結構里不能混用->
和:
,否則會有編譯錯誤。
4. 每一個->
後面只允許接一個表達式、一個代碼塊、或者一個throw
語句。這樣在代碼塊中定義的局部變數,其作用域就限制在代碼塊中,而不是蔓延到整個switch
結構。邏輯更加清楚了。
2. switch作為表達式(expression)
switch
結構一直是一個statement,而從Java 12開始,它也可以用作expression。從學院派的定義理解statement和expression的區別叫人頭疼,如果說人話的話,就是switch
可以有返回值了。
作為statement的switch
沒有返回值,所以我們不能寫出這樣的代碼
x = switch (y) { ... }
如果需要根據不同的條件給某個變數賦值,我們以前只能這樣做
String word = "";
switch (num) { case 1: word = "One"; break; case 2: word = "Two"; break; default: String result = String.format("Other (%d)", num); word = result; }
讓人難受的地方有兩個。
1. 重覆多次地寫賦值語句,繁瑣且易錯。
2. 這段程式的終極目標是為word變數
賦值,而賦值前必須在其他的地方初始化word
,淡化了二者的邏輯關係,代碼也顯得瑣碎。
從12開始我們可以這樣改造代碼
String word = switch (num) { case 1 -> "One"; case 2 -> "Two"; default -> { String result = String.format("Other (%d)", num); break result; } };
可見,switch
成了一個表達式(expression),它有自己的返回值。每一個分支只需要決定具體的返回值是什麼,不需要考慮如何使用這個值。而全程只需要一次賦值操作。代碼整體變得更簡潔、緊湊、清晰。
而返回值又有兩種寫法。還記得嗎,上一節提到過,->
後只能接三樣東西:表達式、代碼塊、throw語句。throw
的情況沒有返回值,先不管它。另外兩種情況:
1. 如果分支只有一個表達式,那麼表達式本身就是switch
的值,比如上面例子里的"One"
和"Two"
;
2. 如果分支是一個代碼塊,比如例子中的default
,可以看到Java 12改造了break
關鍵字,可以通過break result
的形式返回值。switch
並沒有拋棄break
,而是賦予它更重要的職能。
作為expression的switch
也可以使用:
,在這種情況下,各個分支必須用break
關鍵字返回值。像這樣
String word = switch (num) { case 1 : break "One"; case 2 : break "Two"; default : { String result = String.format("Other (%d)", num); break result; } };
上面例子中,case 1
和case 2
中的break
不能省略,否則會有編譯錯誤。
很顯然,當switch
用作expression時,每一個分支都必須有返回值(或者有throw
異常)。我們不能寫下麵這樣的代碼
String word = switch (num) { case 1 -> "One"; case 2 -> "Two"; default -> { System.out.println("莫挨老子"); //錯誤: switch rule completes without providing a value } };
編譯器不知道當num=3的時候應該返回什麼,於是它憤怒地拋出了一個錯誤。
最後要強調,switch
在不返回值的時候,還是一個statement。而作為expression並且在一句代碼的結尾處時,不要忘了後面的分號!(親自踩坑,友情提醒)
To be continue...
可能你會覺得這些改進還是小修小改,不值得過分激動。但是,JEP 325是JEP 305: Pattern Matching的依賴。雖然沒有最終確定,但或許Pattern Matching會在不久後的幾個版本正式引入,到時又將是語言層面的大革命。後續的幾個版本還是值得期待的。