動機
之所有要研究是否有可能在Java中加入模式匹配,主要還是為了改進Java的語言特性。假如有這樣的一段代碼:
if (obj instanceof Integer) { int intValue = ((Integer) obj).intValue(); // 使用intValue }這段代碼做了三個操作:
判斷obj是否是一個Integer類型 將obj轉成Integer類型 從Integer中抽取出int現在再來看看在if...else結構的語句中判斷其他類型。
String formatted = "unknown";if (obj instanceof Integer) { int i = (Integer) obj; formatted = String.format("int %d", i); }else if (obj instanceof Byte) { byte b = (Byte) obj; formatted = String.format("byte %d", b); }else if (obj instanceof Long) { long l = (Long) obj; formatted = String.format("long %d", l); }else if (obj instanceof Double) { double d = (Double) obj; formatted = String.format("double %f", d); }else if (obj instanceof String) { String s = (String) obj; formatted = String.format("String %s", s); }...雖然上述的代碼是可運行的,也很好理解,但寫起來很枯燥(太多重復的樣板代碼),也容易產生bug。過多的樣板代碼會讓業務邏輯變得含糊不清——如果instanceof方法已經判斷出傳入的實例是何種類型,那么就沒必要重復進行轉型了。
Goetz和Bierman解釋了他們想要做出的改進。
我們認為是時候讓Java擁抱模式匹配了,而不僅僅是把它作為一種臨時的解決方案。很多編程語言從60年代開始就已經采用了模式匹配,包括面向文本的編程語言SNOBOL4和AWK、函數編程語言Haskell和ML,以及最近的面向對象編程語言Scala和C#。
模式由判斷謂語(predicate)和一系列綁定變量(binding variable)組成,判斷謂語被應用在目標上面,而綁定變量是從目標中抽取出來的。
if (x matches Integer i) { // 使用i }Goetz和Bierman使用過各種模式匹配,它們使用了關鍵字matches和exprswitch。
matches操作符
matches操作符可以用來代替instanceof操作。例如:
String formatted;switch (obj) { case Integer i: formatted = String.format("int %d", i); break; case Byte b: formatted = String.format("byte %d", b); break; case Long l: formatted = String.format("long %d", l); break; case Double d: formatted = String.format(“double %f", d); break; default: formatted = String.format("String %s", s); }...只有當變量x與某個Integer實例匹配時,變量i才能被使用。如果把Integer類型擴展到其他類型,那么if...else結構里的類型轉換就可以省掉了。
switch的改進
Goetz和Bierman解釋說,“switch語句就是一種最完美的模式匹配”。例如:
String formatted;switch (obj) { case Integer i: formatted = String.format("int %d", i); break; case Byte b: formatted = String.format("byte %d", b); break; case Long l: formatted = String.format("long %d", l); break; case Double d: formatted = String.format(“double %f", d); break; default: formatted = String.format("String %s", s); }...上面的代碼清晰易懂。不過,Goetz和Bierman也指出了switch的一個局限——“它只是一個語句,所以分支也必須是語句。我們希望可以把它們變成三元操作符那樣的表達式,這樣就可以保證只對其中的一個表達式求值”。
他們建議引入一種新的表達式語句——exprswtich。
String formatted = exprswitch (obj) { case Integer i -> String.format("int %d", i); case Byte b -> String.format("byte %d", b); case Long l -> String.format("long %d", l); case Double d -> String.format(“double %f", d); default -> String.format("String %s", s); };...Goetz和Bierman建議的模式如下所述。
類型檢測模式(將被轉型的目標綁定到變量) 解構模式(解構目標并進行遞歸匹配) 常量模式(等值匹配) 變量模式(任意匹配并綁定目標) 下劃線模式(任意匹配)以下是Goetz與InfoQ的談話內容。
InfoQ:在你發布論文后,社區都有哪些反饋?
Goetz:我們收到了非常積極的反饋。在其他語言里使用過模式匹配的人都很喜歡這個特性,他們也希望能夠在Java中使用它。對于那些之前沒有使用模式匹配的人,我們希望他們能夠學會使用這個特性,我們認為很有必要在Java里添加這一特性。
InfoQ:Scala的匹配操作符對Java模式匹配的設計有多大的影響?有什么事情是Scala的匹配操作能做的而Java卻做不到的嗎?
Goetz:Scala只是眾多啟發我們在Java中加入模式匹配的語言之一。為一門語言添加特性不外乎從其他語言那里“移植”,但實際上,我們并不希望做得跟Scala完全一樣,我們只要能夠做到Scala的一部分,同時也能做Scala做不到的。
我們認為我們更有可能將模式匹配深度集成到對象模型中,比Scala有過之而無不及。Scala的模式是靜態的,難以重載或覆蓋。雖說能夠做到這樣已經很好了,但我們希望能夠做得更好。
解構(deconstruction)是構造(construction)的另一面,面向對象編程語言讓我們可以構造對象(構造器、工廠、構建器),而解構將給我們帶來更豐富的API。雖說模式匹配與面向函數語言有一定的歷史淵源,但我們相信它在面向對象語言里將會得到更好的發揚。
人們對語言特性津津樂道,不過我們認為語言特性真正的作用應該是為軟件庫提供更好的服務,而模式匹配將幫助我們寫出更簡單、更安全的軟件庫。
以java.lang.Class的兩個方法為例:
public boolean isArray() { ... } public Class getComponentType() { ... }第二個方法需要以第一個方法返回true作為前提。對于API提供者(需要些更多代碼,也需要更多的測試)和用戶(容易出錯)來說,涉及多API的邏輯操作就意味著復雜性。從邏輯上看,這兩個方法就是一個模式,融合了“判斷這個類是否是一個數組類”和“根據條件抽取組件類型”。如果能夠通過以下的表達式來表達,那么代碼寫起來就更簡單了,而且不容易出錯。
if (aClass matches Class.arrayClass(var componentType)) { ... }InfoQ:這次是否把讓Scala rebase模式匹配作為目標(比如Scala 2.12就基于接口對trait進行了rebase)?
Goetz:與Lambda表達式一樣,我們希望在設計這一語言特性的過程中,能夠找到一些構建塊,并把它們集成到底層的平臺中,讓其他語言也能從中獲益,并為多個語言提供更好的互操作性。
InfoQ:Scala在實現這一特性時添加了很多額外的字節碼用于支持解構case類型,這樣會造成負面影響嗎?
Goetz:這樣做最大的問題在于,編譯器不再只是個編譯器了,它往類成員里添加了語義。雖然這樣做很方便,但可能不是用戶想要的,比如,比較數組要用Arrays.equals(),而不能用Object.equals()。
InfoQ:解構是否僅限于數據類(data class)?
Goetz:我們計劃分幾次來發布模式匹配功能,最開始先發布類型檢測,然后是數據類的解構模式,最后是用戶自定義的解構模式。雖說解構不會僅限于數據類,但這一過程還是需要一些時間。
InfoQ:你能夠解釋一下數據類和值類型(value type)之間的關系嗎?
Goetz:它們之間是一種正交關系。值就是一種合體,沒有標識。通過顯式地拒用標識,運行時可以優化內存布局,扁平化對象頭部,更自由地跨同步點緩存值組件。數據類簡化了類表示和API協定之間復雜的關系,編譯器就可以注入常用的類成員,如構造器、模式匹配器、equals方法、hashCode方法和toString方法。有些類可以被聲明成值類(value class),有些則適合被聲明成數據類,或者都不聲明,或者都聲明,這些情況都有可能。
InfoQ:Sealing特性是否需要源碼編譯器的支持?
Goetz:Sealing特性不僅僅需要編譯器的支持,也需要JVM的支持,這樣語言層面的約束——比如“X不能繼承Y”——就可以在JVM層面得到加強。
InfoQ:Sealing是否意味著“只能在當前模塊內擴展出子類”?
Goetz:Sealing可以有多種說法,最簡單的就是“這個類只能在同一個源碼文件中被擴展”——這是最常見的情況,也是最簡單的。Sealing也可以被定義成“同一個包中”或“同一個模塊中”,甚至可以是“友聯(friend)”或復雜的運行時判斷。
InfoQ:Java的新發布周期有助于模式匹配被集成到Java中嗎?
Goetz:我們希望如此。我們已經將模式匹配分為幾個小塊,這樣就可以快速地推出最簡單的部分,然后繼續開發其他部分。
InfoQ:什么時候可以看到原型?
Goetz:現在就有了,嘗鮮者可以直接從源代碼編譯JDK。“Amber”上有一個分支已經可以支持類型檢測模式和“matches”判斷謂語。
InfoQ:你們將會怎樣繼續關于模式匹配的研究工作?
Goetz:我們會繼續探究如何將匹配器作為類成員,以及如何實現重載和繼承。我們還有很多工作要做。
更多資源
Towards Pattern Matching in Java by Kerflyn, May 9, 2012 Pattern Matching in Java by Benji Weber, May 3, 2014 Pattern Matching in Java with the Visitor Pattern by Kevin Peterson, February 11, 2015 Adventures in Pattern Matching by Brian Goetz, JVM Language Summit, August 2017 Moving Java Faster by Mark Reinhold, September 6, 2017 Java to Move to 6-Monthly Release Cadence by InfoQ, September 6, 2017