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會在不久后的幾個版本正式引入,到時又將是語言層面的大革命。后續的幾個版本還是值得期待的。
