提到模式匹配(Pattern Matching),Java 開發(fā)人員可能會比較陌生。實際上,其他編程語言的開發(fā)人員早就已經(jīng)使用過模式匹配了。JVM 上的編程語言 Scala 的模式匹配功能就很強大。
什么是模式匹配?
為了更好地解釋模式匹配,我們從一個簡單的例子開始。我們希望創(chuàng)建一個方法,可以把任何對象轉(zhuǎn)換成 String 格式。這就需要根據(jù)對象的類型來進行不同的格式化操作。我們可以很容易就寫出下面這樣的代碼。這段代碼的核心是使用 instanceof 操作符來檢查輸入對象的類型,再根據(jù)對象類型進行格式化操作。
public class ObjectFormatter {public String format(Object input) {if (input == null) {return “”;} else if (input instanceof Number) {return NumberFormat.getNumberInstance().format(input);} else if (input instanceof LocalDateTime) {return ((LocalDateTime) input).format(DateTimeFormatter.ISO_DATE_TIME);} else {return input.toString();}}}
上述對 instanceof 操作符的使用就是模式匹配的一種簡單形式。
一個模式由匹配 predicate 和模式變量的集合組成。
- 匹配 predicate 判斷一個模式是否可以匹配目標對象。
- 如果模式匹配的話,模式變量的集合用來從目標對象中提取值。
在 instanceof 操作符的例子中,匹配 predicate 的作用是檢查目標對象的類型,而模式變量的集合中只有一個變量,就是目標對象自身。這種類型的模式,被稱為類型模式(type pattern)。除了類型模式之外,計劃中的模式還包括記錄類型模式和數(shù)組模式。
模式匹配是一個涵蓋范圍非常大的功能。根據(jù)現(xiàn)在 Java 的發(fā)布周期,模式匹配的內(nèi)容會在不同的 Java 版本中逐漸添加進來。具體的發(fā)布周期可以參考下面的表格。這個表格的右側(cè)三列表示的是不同的與模式匹配相關的功能,每一行表示這些功能在對應 Java 版本中的可用狀態(tài)。
Java版本 | instanceof 模式 | switch 的模式匹配 | 記錄類型模式 |
Java 14 | 預覽 | ||
Java 15 | 二次預覽 | ||
Java 16 | 正式功能 | ||
Java 17 | 正式功能 | 預覽 | |
Java 18 | 正式功能 | 二次預覽 | |
Java 19 | 正式功能 | 三次預覽 | 預覽 |
以 Java 17 為例,可以使用 instance 模式的正式功能,以及 switch 模式匹配的預覽功能。
Java 18 和 Java 19 中可用的模式匹配功能也列在了表格中,作為參考。
instanceof 模式匹配
Java 中的 instanceof 操作符用來檢查對象的類型。下面的代碼給出了通常使用 instanceof 操作符的代碼范式。在 if 語句中使用 instanceof 來進行檢查,如果檢查通過,則使用強制類型轉(zhuǎn)換,把輸入對象 obj 轉(zhuǎn)換成 String 類型的 s,最后再使用變量 s。
if (obj instanceof String) {String s = (String) obj;}
從上述代碼中可以看到,對 instanceof 操作符的使用范式是非常繁瑣的,其中需要檢查的目標類型 String 就出現(xiàn)了三次。在使用了 instanceof 模式匹配之后,代碼可以簡化很多。在下面的代碼中, String s 表示類型模式,其中 String 是需要匹配的類型,s 是匹配成功之后用來捕獲目標對象的變量。該變量 s 可以直接在 if 語句塊中使用。
if (obj instanceof String s) {System.out.println(s.toUpperCase());}
模式變量使用的是流作用域(flow scoping)。一個模式變量能夠出現(xiàn)在作用域中,當且僅當編譯器可以推斷出模式匹配必定成功,并且該變量被賦予了一個值時。在上面的例子中,if 語句塊的代碼只有在模式匹配成功了之后才會執(zhí)行,變量 s 此時必定被賦予了值 obj,因此編譯器可以確定 s 必定在 if 語句塊的作用域中。
關于流作用域,其實不用了解太多。如果使用錯誤,編譯器會提示你的。
下面的代碼給出了 instanceof 模式匹配的代碼示例。第一個 if 匹配 String 類型的同時,加上了對字符串長度的檢查;第二個 if 匹配剩下的 String 類型的對象。在第一個 if 的條件中,obj instanceof String s 和 s.length() > 10 的順序不能反過來。這里利用了 && 的短路(short-circuit)特性,當?shù)谝粋€ instanceof 模式匹配成功之后,才會執(zhí)行后面的判斷,這個時候 s 必然是一個 String 對象,可以安全地使用 length 方法;如果第一個 instanceof 模式不匹配,后面的判斷不會被執(zhí)行,因此也不會出現(xiàn)錯誤。
public class StringMatch {public void test(Object obj) {if (obj instanceof String s && s.length() > 10) {System.out.println(“長字符串 -> ” + s);} else if (obj instanceof String s) {System.out.println(“短字符串 -> ” + s);} else {System.out.println(“其他”);}}}
在 switch 語句和表達式中使用模式匹配
在 Java 17 中,switch 語句和表達式的 case 子句中可以使用模式匹配。該功能在 Java 17 中是預覽功能,因此需要通過命令行參數(shù) –enable-preview 來啟用。switch 在很多時候可以替代嵌套的 if/else。
下面的代碼使用 switch 語句加上模式匹配改寫了上面的使用嵌套 if/else 的代碼示例。使用 switch 比 if/else 更加簡潔。這里的 switch 用的是箭頭格式。
public class StringMatch {public void test(Object obj) {switch (obj) {case String s && s.length() > 10 -> System.out.println(“長字符串 -> ” + s);case String s -> System.out.println(“短字符串 -> ” + s);default -> System.out.println(“其他”);}}}
我們可以用 switch 語句改寫文章開頭提到的對象格式化的方法,如下面的代碼所示。使用 switch 語句加上模式匹配的代碼更加簡潔易懂。
public class ObjectFormatter {public String format(Object input) {return switch (input) {case null -> “”;case Number n -> NumberFormat.getNumberInstance().format(n);case LocalDateTime t -> t.format(DateTimeFormatter.ISO_DATE_TIME);default -> input.toString();};}}