本文提供了Java SE 12擴展Switch語句/表達式的完整指南。文章詳細介紹了擴展Java switch語句將其用作增強版switch語句或表達式。爲幫助理解本文提供了具體案例。
本文要點
-
現在Java的switch語句是遵循類似C++這樣的語言而設計的,在默認情況下支持fall-through語法。
-
這種控制流對於編寫低級代碼非常有用。然而,因爲switch是運用在高級別語言環境下的,容易出錯的問題已經慢慢開始蓋過它靈活的優勢。
-
隨着Java構造者開始支持Java語言的模式匹配以來,現在的switch語句的不規則性變成了一個障礙。
-
在Java 12中重新增強switch讓它具備了新的能力,通過擴展現有的switch語句,可將其作爲增強版的switch語句或是“switch表達式”來簡化代碼。
-
這篇文章探索了全新的Java 12 switch語句,並提供了基於JShell的正確使用和不正確使用的例子。
目前沒有不使用switch語句的代碼庫,即使是要做性能調整,相比於if/else語句我們也更傾向於使用switch語句。自Java誕生以來,就已經有Java switch語句了,我們都非常習慣使用它了,甚至能夠接受它的“怪癖”。
現在Java的switch語句是遵循類似C++這樣的語言而設計的,在默認情況下支持fall-through語法。這種控制流對於編寫低級代碼非常有用。然而,因爲switch是運用在高級別語言環境下的,它容易出錯的問題已經慢慢開始蓋過它靈活的優勢。
隨着Java構造者開始支持Java語言的模式匹配以來,現在的switch語句的不規則性變成了一個障礙。問題包括switch塊的默認控制流行爲;switch塊的默認作用範圍,在其中塊被作爲單個作用範圍;以及switch只作爲語句工作。
在Java 12中重新增強switch讓它具備了新的能力,通過擴展現有的switch語句,可將其作爲增強版的switch語句或是“switch表達式”來簡化代碼。
這可以讓“傳統的”或是“簡化的”switch範圍和控制流行爲都可以實現。這些變化將會簡化日常代碼編寫,爲將來使用switch語句或表達式的模式匹配帶來便利。
新功能實踐
自JDK 9以來推出了全新的快速發佈計劃,每6個月發佈一次的計劃幫助Java變得更加有生產力和創新性,功能也會更快更頻繁地發佈。
從JDK 9、10、11,到即將到來的JDK 12(將會在2019年3月19日發佈)及其下一個版本,近來的每次JDK發佈都驗證了這一點,它們總能給我們帶來新的Java語言功能以及很多API的改進。
新項目
爲了支持這樣的改進和新功能,社區需要着重於關注語言某個方面的項目,讓Java語言工程師專注於新的改進,而不是讓所有的東西都在巨大的JDK環境下開發。
因此已經創造了很多項目,這些項目都集中在語言的某個特定方面。你可以在OpenJDK.net網站中的project菜單部分找到它們。這裏僅舉幾個例子,Panama、Valhalla、Amber、Jigsaw、Loom等等。我們在這裏詳細介紹一下Amber項目。
Amber項目
Amber項目由Compiler Group發起的,我們可以從發起者的名字猜測到該項目的目標。是的,你猜的很對,這個項目的目標是探索並培育出更小的、生產力導向的Java語言功能,在OpenJDK JEP過程中被採納爲候選的JEPs。
以下列出的是Amber項目正在開發或已經交付的JEPs列表以及其指定的JDK要求(如果有的話):
正在開發中的JEPs:
-
JEP 302 Lambda Leftovers
-
JEP 305 Pattern Matching for instance of (Preview)
-
JEP 325 Switch Expressions (preview, JDK 12)
-
JEP 326 Raw String Literals (preview)
-
JEP draft 8209434 Concise Method Bodies
已經交付的JEPs:
-
JEP 286 Local-Variable Type Inference (var) (JDK 10)
-
JEP 323 Local-Variable Syntax for Lambda Parameters (JDK 11)
用模式匹配增強Java
模式匹配技術從20世紀60年代開始就已經適用於不同風格的編程語言,包括面向文本的語言如SNOBOL4和AWK,函數式語言如Haskell和ML,最近擴展到面向對象的語言如Scala(甚至最近還到了Microsoft C#語言)。
Amber項目添加了Java語言支持的模式匹配。模式匹配使程序中的通用邏輯可以有條件地從對象中提取組件,來讓它們更簡潔和安全地表達,這會增強許多還在構建中的語言,比如說JEP 305增強了instanceof運算符(還沒有指定任何JDK版本),以及JEP 325的switch表達式(目標是JDK 12)。
使用全新的switch語句
自Java SE 9發佈以來,在交互式環境工具(REPL)JShell中試驗新的Java語言或API功能已經成爲了傳統。它不需要安裝任何IDE或其他的軟件,只需要JDK就足夠了。
由於每個JDK都有這個功能,我們能很容易地試驗新的Java SE 12語言功能,比如說新擴展的switch語句和新的switch表達式。我們只需要下載並安裝JDK 12早期訪問版本,可以通過這個鏈接安裝。
成功下載後,解壓文件到任意選擇的位置,讓JDK的bin文件夾可以從你係統的任何位置訪問。
爲什麼是JShell?*
我通常喜歡使用交互式編程環境工具,就像大多數現代語言使用的工具一樣,方便快速學習Java語言語法,瞭解全新Java API和其功能,甚至原型化複雜的代碼。
這並不是冗長的編輯、編譯和執行代碼的循環過程,通常會包括以下幾個過程:
-
編寫完整的代碼。
-
編譯它並修復任何錯誤。
-
運行程序。
-
瞭解發生了什麼錯誤。
-
編輯它。
-
重複這個過程。
什麼是JShell?
現在Java有了JShell工具之後擁有了豐富的REPL(讀取、求值、輸出、循環)實現,這稱爲Java Shell,是交互式編程環境。那麼,它的神奇之處在哪裏呢?其實非常簡單。JShell提供了快速友好的環境,幫助你快速探索、發現並試驗Java語言功能以及其豐富的庫。
使用JShell,你可以一次輸入一個程序元素,就可以立即看到結果,並根據需要進行調整。在REPL中你不需要編寫完整的代碼,你可以寫JShell指令和Java代碼片段。
InfoQ最近發表的對於JShell的介紹可以幫助你全面瞭解該工具。想要深入瞭解學習JShell的所有功能,我錄製了名爲“通過JShell實現Java 10編程”的視頻,可以幫助你掌握這個主題,可以從Packt網站查看。
開始JShell會話
與該工具交互的第一件事就是打開新的JShell會話,如下所示:
-
在Microsoft Windows系統下,僅需打開命令提示符,輸入jshell後回車。
-
在Linux系統下,打開shell窗口,輸入jshell後回車。
-
如果你用的是macOS(之前的OS X)系統,則打開Terminal窗口,輸入”jshell”指令,然後回車。
哦耶!該命令打開了新的JShell會話,並在jshell>提示符顯示如下信息:
mohamed_taman:~$ jshell --enable-preview
| Welcome to JShell -- Version 12-ea
| For an introduction type: /help intro
jshell>
在上面的第一行中,“Version 12-ea”表示你在使用Java SE JDK 12搶先體驗版。JShell的提示性消息用豎線(|)標識,現在你就可以準備輸入任何Java代碼或JShell指令和片段。
我相信你已經注意到了enable-preview選項。這個選項是幹嘛的?聽着我的朋友,這個選項幫助你解鎖現在在“預覽”階段的任何新語言功能,它們還沒有正式成爲JDK的一部分。這些功能在默認情況下是無法使用的,因爲它們還在試驗和反饋階段。
使用這個選項可以幫助開發人員試用預覽功能,收集你的反饋來持續改進,你還可以反饋你覺得不好用的部分。在這篇文章中,我們將繼續探索新的switch表達式功能,這就是爲什麼我們在打開JShell會話的時候要引入該選項。
注意:由於這還是一個預覽功能,你還可以在Amber郵件列表中給出你的反饋。
理論部分已經介紹的足夠多了,接下來讓我們嘗試一些新的switch語句或表達式的片段,來幫助理清概念,學習如何使用這個新功能。
剖析switch語句和表達式
首先,我們要搞清楚現狀,瞭解當前版本的switch語句,甚至瞭解我們在日常寫代碼中的奇怪用法。
讓我們簡單的把Day作爲變量,枚舉Day(週六、週日、週一等等),基於此返回“週末”還是“工作日”。
在前面打開的JShell會話中,讓我們開始定義Day enum:
jshell> enum Day{
...> NONE,
...> SAT,
...> SUN,
...> MON,
...> TUS,
...> WED,
...> THU,
...> FRI;
...> }
| created enum Day
接下來,定義我們的方法String whatIsToday (Day day)來切換(我們使用的是普通的switch)day的值並返回它是工作日還是週末,如下所示:
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT:
...> case SUN: today = "Weekend day";
...> break;
...> case MON:
...> case TUS:
...> case WED:
...> case THU:
...> case FRI: today = "Working day";
...> break;
...> default: today = "N/A";
...> }
...> return today;
...> }
| created method whatIsToday(Day)
jshell> whatIsToday(Day.SUN)
$3 ==> "Weekend day"
儘管這個方法可以正常運作,如果我們使用每個情況都有一個break語句的原始版本,就會給視覺帶來紊亂,讓錯誤調試變得困難。還會讓代碼變得冗長,這根本沒必要,但少一個break語句就會導致發生fall-through。
在上面的例子中我們通過fall-through機制歸類有相同值的幾個日子來減少break語句的使用,但代碼還是很長。
傳統switch語句的改進
感謝JEP 325中提出了全新的”箭頭”(switch標籤規則)形式,寫爲”case L ->”來表示如果標籤匹配,僅執行標籤右邊的代碼,而且它可以和switch語句或表達式一起使用。
同樣的,傳統的”colon”語法(switch標籤語句組)也可以用於這兩個情況。
儘管colon和全新的箭頭語法都適用於這兩種情況,colon(:)的出現並不代表着這就是switch語句,箭頭(->)的出現也不代表着這就是switch表達式。
現在讓我們實踐一下之前所講的理論,比如說,之前的String whatIsToday(Day day)可以簡化爲下面的簡單版本:
jshell> /edit whatIsToday
通過運行之前的JShell /edit指令,它將打開JShell編輯板,更方便地修改大塊代碼段,比如這個方法,這樣我們就在JShell編輯板中修改代碼,顯示如下:
String whatIsToday(Day day){
var today = "";
switch(day){
case SAT, SUN -> today = "Weekend day";
case MON, TUS, WED, THU, FRI -> today = "Working day";
default -> throw new IllegalArgumentException("Invalid day: " + day.name());
}
return today;
}
當你結束脩改之後,點擊Exit按鈕來保存修改並返回當前的JShell>會話,輸入一些值進行試驗:
jshell> /edit whatIsToday
| modified method whatIsToday(Day)
jshell> whatIsToday(Day.SUN)
$5 ==> "Weekend day"
jshell> whatIsToday(Day.MON)
$6 ==> "Working day"
jshell> whatIsToday(Day.NONE)
| Exception java.lang.IllegalArgumentException: Invalid day: NONE
| at whatIsToday (#11:6)
| at (#12:1)
jshell>
如果你仔細研究一下之前新的switch語句結構,你會注意到這裏有很多變化,首先,它更加清晰、簡潔並且沒有”break”語句,不是嗎?
另外,你會發現switch語句利用新的”箭頭”語法(標籤規則)形式來完成切換,而不需要顯式地指定break,這就避免了我們擔心的switch fall-through情況。
需要注意的是”case L ->”switch標籤右邊的代碼嚴格規定是表達式、塊或(爲了方便)throw語句,就像我們在前面的方法中將識別到的非法的日期拋出IllegalArgumentException。
單個switch情況下的多個逗號分隔標籤
在傳統的switch語句中,當我們需要將多個情況執行相同的一組語句的時候,我們會使用fall-through機制。
但這裏,我們要提醒大家需要使用全新的“單個switch情況下的多個逗號分隔標籤”,只需要用逗號分隔就可以執行相同的語句。
Switch標籤語句組
根據JEP 325的說法,傳統的”colon”語法(switch標籤語句組)還可以正常使用,我們用colon的形式重寫之前的方法如下所示:
String whatIsToday(Day day){
var today = "";
switch(day){
case SAT, SUN: today = "Weekend day"; break;
case MON, TUS, WED, THU, FRI: today = "Working day"; break;
default: throw new IllegalArgumentException("Invalid day: " + day.name());
}
return today;
}
我會把這部分留給你自己做小練習。你可以自己嘗試一下,運行JShell指令/edit whatIsToday,將現在的方法轉變爲箭頭符號語法,使用colon/break語法改寫之前的方法,輸入幾個值試一試,這樣你能更深入地瞭解它。
新的switch表達式
在我們深入研究之前,你可能會想在switch作爲表達式之前是什麼樣的。在Java SE 12之前,switch一直是語句,它永遠都是控制流的結構,不會賦給什麼東西。另外,表達式總是能精確得到一個值的結果。因爲計算的最終目的都是結果,將值賦給某個目的地。
我們討論過了語句和表達式的區別,讓我們看看新的switch表達式是如何工作的。實際上,很多日常生活中使用的現有switch語句,甚至是上面的代碼,都是switch表達式的模擬,每個分支要麼賦值給某個公共目標變量,要麼返回值。
現在我們可以用新的switch表達式將值直接返回給要賦值的目標變量,我們來修改一下StringwhatIsToday(Day day)方法:
String whatIsToday(Day day){
var today = switch(day){
case SAT, SUN -> "Weekend day";
case MON, TUS, WED, THU, FRI -> "Working day";
default -> throw new IllegalArgumentException("Invalid day: " + day.name());
};
return today;
}
仔細來看一下修改之後的方法,我們會發現我們將switch語句作爲表達式來寫:首先,switch是寫在等號之後的;其次,它是語句的一部分,需要以分號結尾,但是傳統的switch語句並不需要;第三,箭頭符號之後是返回值。
就像我之前所說的一樣,我們可以使用傳統的”colon”語法”case L:”在新的switch表達式中,我們可以用value語句重寫之前使用break的方法,如下所示:
String whatIsToday(Day day){
var today = switch(day){
case SAT, SUN: break "Weekend day";
case MON, TUS, WED, THU, FRI: break "Working day";
default: throw new IllegalArgumentException("Invalid day: " + day.name());
};
return today;
}
兩種形式的break(有或沒有值)都類似於方法中兩種形式的返回。
語句塊
儘管我們在日常工作中使用新的switch語句/表達式會使用”colon”或”箭頭”語法,在大多數情況下我們還是會使用兩種形式任意一個右側的單個表達式。
然而會有情況需要同時處理多個語句。我們可以簡單地使用塊{}來實現,我們可以多次在一個switch語句/表達式中創造並使用相同的變量名,如下所示:
String whatIsToday(Day day){
var today = switch(day){
case SAT, SUN: break "Weekend day";
case MON, TUS, WED, THU, FRI:{
var kind = "Working day";
break kind;
}
default: {
var kind = day.name();
System.out.println(kind);
throw new IllegalArgumentException("Invalid day: " + kind);
}
};
return today;
}
它是poly表達式
Switch語句是“poly表達式”,如果目標類型已知,這個類型可以下推到每個case分支中,否則,會綜合考慮每個case分支推算出一個獨立的類型。
想想以下的switch表達式賦值:
jshell> var day = Day.SUN
day ==> SUN
jshell> var today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> {
...> var len = day.name().length();
...> break len;
...> }
...> };
today ==> "Weekend day"
jshell> var day = Day.NONE
day ==> NONE
jshell> var today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> {
...> var len = day.name().length();
...> break len;
...> }
...> };
4
today ==> 4
如果我們進一步分析上面的switch表達式,我們會發現兩個分支返回String,默認分支返回int變量len。綜合這兩種情況,指定目的變量爲var類型。
現在,我們顯式地聲明目標賦值類型是int而不是var,這會讓編譯器想方設法滿足“如果目標類型已知,這種類型會下推到每個case分支”這種情況:
jshell> int today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> {
...> var len = day.name().length();
...> System.out.println(len);
...> break len;
...> }
...> };
| Error:
| incompatible types: bad type in switch expression
| java.lang.String cannot be converted to int
| case SAT, SUN -> "Weekend day";
| ^-----------^
| Error:
| incompatible types: bad type in switch expression
| java.lang.String cannot be converted to int
| case MON, TUS, WED, THU, FRI -> "Working day";
| ^-----------^
結果如上所示,會報出錯誤:不兼容的類型,說java.lang.String 不能轉換爲int類型,並給出了返回值是String 類型的兩個case 分支。
新的switch特性做不了什麼
現在讓我們來想一下,我們寫什麼樣的switch語句或表達式代碼會讓編譯器停止運作,以及怎麼才能避免這樣的情況,保證編譯器正常運轉。
Break不能返回switch語句中的值
如果你嘗試在一個break語句中使用switch語句返回的值,這就會造成編譯時錯誤:
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT, SUN: break "Weekend day";
...> case MON, TUS, WED, THU, FRI: break "Working day";
...> default: throw new IllegalArgumentException("Invalid day: " + day.name());
...> }
...> return today;
...> }
| Error:
| unexpected value break
| case SAT, SUN: break "Weekend day";
| ^------------------^
| Error:
| unexpected value break
| case MON, TUS, WED, THU, FRI: break "Working day";
| ^------------------^
| Error:
| unreachable statement
| return today;
| ^-----------^
jshell>
箭頭語法只指向switch語句中的語句
同樣,如果你嘗試使用switch語句和箭頭語法來返回值,也會造成編譯時錯誤。在這種情況下,箭頭語法應該只指向語句而不返回值:
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> throw new IllegalArgumentException("Invalid day: " + day.name());
...> }
...> return today;
...> }
| Error:
| not a statement
| case SAT, SUN -> "Weekend day";
| ^-----------^
| Error:
| not a statement
| case MON, TUS, WED, THU, FRI -> "Working day";
不允許混合使用colon和break語法
如果你嘗試混合使用“箭頭”和傳統的colon/break語法,編譯器就會報錯:
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT, SUN -> today = "Weekend day";
...> case MON, TUS, WED, THU, FRI: today = "Working day";break;
...> default -> throw new IllegalArgumentException("Invalid day: " + day.name());
...> }
...> return today;
...> }
| Error:
| different case kinds used in the switch
| case MON, TUS, WED, THU, FRI: today = "Working day";break;
| ^--------------------------------------------------------^
所有匹配情況都要包含在switch表達式中
最後,如果特定測試變量的switch表達式中沒有包含所有的switch情況,會產生編譯時錯誤。如果你沒有涵蓋所有的情況,會發生這樣的錯誤:
jshell> String whatIsToday(Day day){
...> var today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> };
...> return today;
...> }
| Error:
| the switch expression does not cover all possible input values
| var today = switch(day){
| ^-----------...
在switch表達式中涵蓋所有情況並不容易,這一點會讓人很鬱悶,但是要修復它卻不難,只需要添加一個默認case,它就能作爲一個分支正常編譯了。你可以親自嘗試將上面的代碼修改正確。
Switch的未來
和之前的switch一樣,新改進的switch語句和switch表達式都能很好地完成枚舉。所以,其他類型呢?新的增強的“傳統”和“表達式”形式很類似,它們都可以切換String、int、short、byte、char和封裝類型,到目前爲止並沒有變化。
在將來的Java版本中擴展switch是一個目標,比如說允許切換float、double和long(和它們的封裝類型),這是之前做不到的。
總結
在本文中,你瞭解了Amber項目,模式匹配以及JEP 325是如何擴展switch語句的,幫助它作爲語句和表達式進行使用,這兩種形式都可以使用“傳統”或“簡化的”範圍和控制流行爲。
這些變更還能簡化日常代碼工作,可以避免你忘記添加相關的break語句導致的fall-through的錯誤。在上文中,我已經列出了開發人員開始使用新的switch語句/表達式是可能會犯的錯誤。
另外,這個語言的變更也是爲了switch語句/表達式的模式匹配(JEP 305)做準備,當前的語言進行了擴展以支持更多的原始類型,比如float、double和long以及其封裝類型。
這篇文章接近尾聲,寫文章是一種快樂,我希望你也會喜歡讀這篇文章!如果你喜歡這篇文章,請點擊“喜歡”按鈕,並向朋友或在社交媒體中傳播這篇文章!
資源
JEP 305: Pattern Matching for instanceof
Hands-on Java 10 Programming with JShell
Getting Started with Clean Code Java SE 9
作者簡介
Mohamed Taman是@Comtrade數字服務高級企業架構師、Java Champion、Oracle Groundbreaker Ambassador、採用Java SE.next()、JakartaEE.next()、是JCP成員。曾經是JCP Executive Committee成員、JSR 354, 363 & 373 Expert Group成員、EGJUG領導人、Oracle Egypt Architects Club董事會成員、寫Java、喜愛移動、大數據、雲、區塊鏈和DevOps領域。他是國際講師,“JavaFX essentials”、“Getting Started with Clean Code, Java SE 9”、"Hands-On Java 10 Programming with JShell"等書和視頻的作者、編寫了新書“Secrets of a Java Champions”、獲得了Duke’s choice 2015, 2014大獎、以及JCP outstanding adopt-a-jar participant 2013大獎。
查看英文原文:The Complete Guide to the Java SE 12 Extended Switch Statement/Expression