Java 新特性解析:模式匹配

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文要點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java SE 14(於2020年3月發佈)引入了一種模式匹配作爲預覽特性,將成爲Java SE 16(將於2021年3月發佈)的一項永久性特性。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模式匹配的第一階段僅限於一種模式(類型模式)和一種語言構造(instanceof),但這只是整個完整特性的第一部分。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單地說,模式匹配可以幫我們減少繁瑣的條件狀態提取。在進行條件狀態提取時,我們問一個與某個對象有關的問題(比如“你是Foo嗎”),如果答案是肯定的,我們就從對象中提取狀態:“if (x instanceof Integer i) { ... }”,其中i是綁定變量。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綁定變量需要進行明確的賦值,但比這個更進一步:綁定變量的作用域是程序中將被明確賦值的一組位置。這個叫作流式作用域。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模式匹配是一個強大的特性,將在幾個Java版本中發揮重要作用。未來的部分特性將爲我們帶來switch的模式、Record的解構模式,等等,目的是讓解構對象變得像構造對象一樣容易(在結構上更相似)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java SE 14(於2020年3月發佈)引入了一種"},{"type":"link","attrs":{"href":"https:\/\/openjdk.java.net\/jeps\/305","title":"","type":null},"content":[{"type":"text","text":"模式匹配"}]},{"type":"text","text":"作爲預覽特性,將成爲"},{"type":"link","attrs":{"href":"https:\/\/openjdk.java.net\/jeps\/394","title":"","type":null},"content":[{"type":"text","text":"Java SE 16"}]},{"type":"text","text":"(將於2021年3月發佈)的一項永久性特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模式匹配的第一階段僅限於一種模式(類型模式)和一種語言構造(instanceof),但這只是整個完整特性的第一部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單地說,模式匹配可以幫我們減少繁瑣的條件狀態提取。在進行條件狀態提取時,我們問一個與某個對象有關的問題(比如“你是Foo嗎”),如果答案是肯定的,我們就從對象中提取狀態:“if (x instanceof Integer i) { ... }”,其中i是綁定變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用instanceof獲取對象類型是一種條件提取形式,在獲得到對象類型之後,總是要將對象強制轉換爲該類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以在java.util.EnumMap的複製構造函數中看到一個典型的例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"public EnumMap(Map m) {\n if (m instanceof EnumMap) {\n EnumMap em = (EnumMap) m;\n \/\/ optimized copy of map state from em\n } else {\n \/\/ insert elements one by one\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"構造函數接收另一個Map作爲參數,它可能是也可能不是一個EnumMap。如果是的話,構造函數可以將其強制轉換爲EnumMap,並使用更有效的方法複製Map的狀態,否則的話,它將使用一般的插入方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種“測試並進行強制轉換”的習慣用法是多餘的嗎?在有了m instanceof EnumMap之後,我們還可以做些什麼?模式匹配可以將測試和強制轉換合併到單個操作中。類型模式將類型名稱與綁定變量的聲明組合在一起,如果instanceof成功,綁定變量將被綁定到對象的窄化類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"public EnumMap(Map m) {\n if (m instanceof EnumMap em) {\n \/\/ optimized copy of map state from em\n } else {\n \/\/ insert elements one by one\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的例子中,EnumMap em是一種類型模式(看起來像是一種變量聲明)。我們擴展了instanceof,讓它可以接受模式和普通類型。我們先測試m是不是一個EnumMap,如果是,則將其轉換爲EnumMap,並將結果綁定到if語句第一行中的em變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在instanceof之後必須進行顯式的類型轉換,這是一種繁瑣的操作,而融合這些操作的好處不僅僅是爲了簡潔(儘管簡潔是美好的),它還消除了一個常見的錯誤來源。在剪切和粘貼instanceof及強制轉換代碼,容易在修改了instanceof的類型之後忘記修改強制轉換類型,這就給了漏洞一個藏身之處。通過消除這個問題,我們可以消滅所有這種類型的bug。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一個需要經常進行“測試後強制轉換”的地方是在實現Object::equals時。IDE可能會爲Point類生成equals()方法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"public boolean equals(Object o) {\n if (!(o instanceof Point))\n return false;\n Point p = (Point) o;\n return x == p.x && y == p.y;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是使用模式匹配的等效代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"public boolean equals(Object o) {\n return (o instanceof Point p)\n && x == p.x && y == p.y;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段代碼起到同樣的效果,但更簡單直接,因爲我們可以只使用一個複合布爾表達式來表達一個等效的條件,而不是使用控制流的語句。綁定變量p只在明確被賦值的作用域內生效,例如與&&連接的表達式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果模式匹配可以消除Java代碼中99%的強制類型轉換操作,那麼它肯定會很流行,但模式匹配不僅限於此。隨着時間的推移,將會出現其他類型的模式,它們可以進行更復雜的條件提取,使用更復雜的方式來組合模式,以及提供其他可以使用模式的構造(比如switch,甚至是catch)。再加上"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/articles\/java-14-feature-spotlight\/","title":"","type":null},"content":[{"type":"text","text":"Record"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/articles\/java-sealed-classes\/","title":"","type":null},"content":[{"type":"text","text":"封印類"}]},{"type":"text","text":"的相關特性,模式匹配有可能簡化我們編寫的大部分代碼。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"綁定變量的作用域"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模式包含一個測試操作,如果測試成功,則從對象中有條件地提取狀態,並聲明綁定變量,用於接收提取結果。到目前爲止,我們已經看到了一種模式:類型模式。它們使用T t來表示,其中測試操作爲instanceof T,其中有一個要提取狀態的元素(將對象引用轉換爲T), t是用於接收轉換結果的變量名稱。目前,模式只能放在instanceof的右側。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模式的綁定變量是“普通”的局部變量,但它們有兩個新奇的地方:聲明的位置和作用域。我們習慣於通過語句(Foo f = new Foo())或語句頭部(例如for循環和try-with-resources代碼塊)在“最左側”聲明局部變量,而模式是在語句或表達式的“中間”聲明局部變量,這可能需要一點時間來適應:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (x instanceof Integer i) { ... }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出現在instanceof右邊的i實際上是聲明的局部變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綁定變量的另一個新奇的地方是它們的作用域。“普通”局部變量的作用域從它被聲明的位置開始,直到聲明它的語句或塊的結束。局部變量受制於明確的賦值,這是一種基於流的方式,當我們不能證明它已經被賦值時,就不能讀取它。綁定變量也一樣,但它們更進一步:綁定變量的作用域是程序中將被明確賦值的一組位置。這個叫作流式作用域。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們已經看過流式作用域的一個簡單示例。在Point的equals方法中,我們看到:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"return (o instanceof Point p)\n && x == p.x && y == p.y;\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綁定變量p是在instanceof表達式中聲明的,因爲&&是一種短路操作,如果instanceof返回true,代碼只執行到x == p.x,所以p在表達式x == p.x中得到了明確賦值,因此p此時的作用域就到此爲止。但是,如果我們使用||替換&&,就會得到一個錯誤,說p不在作用域內,因爲它可能在第一個||不是true的情況下到達第二個||表達式,此時p不會被賦值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似地,如果模式匹配出現在if語句的頭部,變量綁定將出現在if語句其中的一個分支中,而不是同時出現在兩個分支中:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (x instanceof Foo f) {\n \/\/ f in scope here\n}\nelse {\n \/\/ f not in scope here\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似地:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (!(x instanceof Foo f)) {\n \/\/ f not in scope here\n}\nelse {\n \/\/ f in scope here\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲綁定變量的作用域是綁定到控制流的,所以反轉if條件或應用德摩根定律將以完全相同的方式轉換作用域,就像它們轉換控制流一樣。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有人可能會想,既然我們可以繼續使用傳統的“作用域一直到塊的末尾”規則,爲什麼要選擇這種更復雜的作用域方法。答案是:我們可以這樣,但我們可能並不喜歡這樣的結果。Java禁止使用局部變量來跟蹤局部變量,如果綁定變量的作用域一直運行到塊的末尾,那麼對於這樣的情況:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (x instanceof Integer num) { ... }\nelse if (x instanceof Long num) { ... }\nelse if (x instanceof Double num) { ... }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就會導致重新聲明num,我們不得不爲每一個分支使用一個新的名字(switch中的case標籤模式也是如此)。通過在執行else時將num置於作用域之外,我們可以在else子句中自由地重新聲明一個新的num(具有新的類型)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"流式作用域在處理綁定變量作用域是否跳出其聲明語句方面可以獲得最好的效果。在上面的if-else示例中,綁定變量在if-else其中的一個分支作用域中,而不是兩者兼有,也不在if-else後面的語句中。但是,如果其中一個分支總是有突發情況(例如返回或拋出異常),我們可以用它來擴展綁定變量的作用域——這通常是我們想要的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們有下面這樣的代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (x instanceof Foo f) {\n useFoo(f);\n}\nelse\n throw new NotFooException();\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段代碼沒有問題,但有點麻煩。很多開發者更喜歡將其重構成:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (!(x instanceof Foo f))\n throw new NotFooException();\nuseFoo(f);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兩者的作用是一樣的,但後者減少了閱讀代碼的認知負擔。關鍵的代碼路徑就在最外層,而不是從屬於某個if(或者更糟糕的是,一個深度嵌套的if),在我們的感知中處於前端和中心。此外,通過在進入方法時檢查先決條件,並在先決條件失敗時拋出異常,閱讀代碼的人就不需要在頭腦中保留“如果它不是Foo該怎麼辦”的場景,因爲前置條件失敗已經提前處理過了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在後者的代碼中,f的作用域涵蓋了方法的剩餘部分,因爲它經過明確的賦值:如果x不是Foo,if就會拋出異常,就不會到達useFoo(),如果x是Foo,就會將x強制轉換類型後的結果賦值給f。但是,如果沒有流式作用域,我們就必須這樣寫:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (!(x instanceof Foo f))\n throw new NotFooException();\nelse {\n useFoo(f);\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於這種情況,一些開發者不僅會因爲關鍵代碼被縮進到else塊中而感到惱火,而且隨着先決條件數量的增加(特別是當一個條件依賴於另一個條件時),結構會變得更加複雜,關鍵代碼也會越來越向右移。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一個需要考慮到的新問題是模式匹配與泛型的相互作用。在我們的EnumMap示例中,我們不僅要測試目標對象的類,還要測試它的類型參數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"public EnumMap(Map m) {\n if (m instanceof EnumMap em) {\n \/\/ optimized copy of map state from em\n }\n ...\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道,在Java中,泛型會被擦除的,我們不能問運行時無法回答的問題。這是怎麼回事?這裏的類型測試有一個靜態組件和一個動態組件。編譯器知道EnumMap是Map的子類型(從EnumMap的聲明看出來:class EnumMap implements Map),所以如果一個Map是EnumMap(動態測試),那它一定是EnumMap。編譯器檢查類型模式中的類型參數是否與目標的已知信息一致。如果將目標轉換爲要測試的類型會導致未檢查的轉換異常,則不允許使用該模式。因此,我們可以在instanceof中使用類型參數,但僅限於可以靜態驗證其一致性的情況。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"未來的方向"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到目前爲止,這種模式匹配特徵是極其有限的,只有一種模式(類型模式)和一種可以使用模式的地方(instanceof)。但即使能力有限,我們也已經獲得了一個顯著的好處:冗餘的強制轉換消失了,消除了冗餘代碼,使更重要的代碼得到了更清晰的關注,同時消除了隱藏bug的地方。但這只是模式匹配將給Java帶來的好處的開始。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然,下一個需要添加模式匹配支持的地方是switch語句,這個語句目前僅限於一組狹窄的類型(數字、字符串和枚舉)和一組狹窄的條件(常量比較)。除了常量之外,在case中引入模式戲劇性地增加了switch的表達能力,我們可以對所有類型進行switch,並表達更有趣的多路條件語句,而不僅僅是一組常量的比較。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Java中引入模式匹配的一個更重要的原因是,它爲我們提供了一種更有原則的方法,可以將聚合分解爲狀態組件。Java的對象通過聚合和封裝爲我們提供了抽象。通過聚合,我們將數據從特定抽象成一般,而封裝幫助我們確保聚合數據的完整性。但是,我們卻常常爲此付出高昂的代價。爲了讓使用者可以查詢對象的狀態,我們需要提供API,以一種受控的方式(比如提供訪問器)查詢對象的狀態。但這些用於狀態訪問的API通常是臨時性的,創建對象的代碼與分解對象的代碼看起來完全不一樣(我們用new Point(x,y)來構造一個Point,但卻通過調用getX()和getY()來恢復狀態)。模式匹配通過將解構引入到對象模型中來解決這個長期存在的差別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這方面的一個例子是Record的解構模式。Record是一種簡潔的透明數據類。這種透明性意味着它們的構造是可逆的。正如Record可以自動獲取大量的成員(構造函數、訪問器、Object方法),它們也可以自動獲得解構模式,也就是所謂的“逆向構造函數”——構造函數將狀態聚合成一個對象,解構模式將對象解構回狀態。如果我們用下面的代碼構造一個Shape:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"Shape s = new Circle(new Point(3, 4), 5);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就可以用下面的代碼來解構:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (s instanceof Circle(Point center, int radius)) {\n \/\/ center and radius in scope here\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Circle(Point center, int radius)是一個解構模式。它檢查目標對象是否爲Circle,如果是,則將其轉換爲Circle,並提取中心和半徑組件(如果是Record,它會通過調用相應的訪問器方法來實現)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解構模式也爲組合提供了機會。Circle的Point組件本身也是一個可以解構的聚合體,我們可以用嵌套的方式表示爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"if (s instanceof Circle(Point(int x, int y), int radius) {\n \/\/ x, y, and radius all in scope here\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在提取Circle的center組件之後,我們進一步將結果與Point(var x, var y)模式匹配。這裏有幾個對稱的地方。首先,構造和解構的句法表達在結構上是相似的——我們可以使用相似的語法來構建對象,並將其分解。(如果是Record,兩者都可以基於Record的狀態描述來獲得。)在此之前,這是一種顯著的不對稱。我們用構造函數構建對象,然後通過API調用(比如getter)把它們拆開,與用於聚合的習慣性用法一點也不一樣。這種不對稱給開發者增加了認知負擔,也給漏洞提供了藏身之處。其次,構造和解構現在以相同的方式進行組合——我們可以在Circle的構造函數中嵌套Point的構造函數,也可以在Circle的解構模式中嵌套Point的解構模式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Record、封印類和解構模式以一種令人愉快的方式協同工作。假設我們有這樣一組表達式樹:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"sealed interface Node {\n record ConstNode(int i) implements Node { }\n record NegNode(Node n) implements Node { } \n record AddNode(Node left, Node right) implements Node { }\n record MultNode(Node left, Node right) implements Node { }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以用switch寫一個計算器,如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"int eval(Node n) {\n return switch (n) {\n case ConstNode(int i) -> i;\n case NegNode(var node) -> -eval(node);\n case AddNode(var left, var right) -> eval(left) + eval(right);\n case MulNode(var left, var right) -> eval(left) * eval(right);\n \/\/ no default needed, Node is sealed and we covered all the cases\n };\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用switch比if-else更簡潔,更不容易出錯,而且switch表達式知道,如果我們已經覆蓋了一個封印類所有允許的子類型,那麼就得到了總數,不再需要默認的case。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Record和封印類有時候被稱爲代數數據類型。通過在Record上添加模式匹配並對模式進行switch,我們可以安全而簡便地抽取代數數據類型。(具有內置元組和sum類型的語言也往往提供了內置的模式匹配,這並非偶然。)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模式匹配的路線圖比這裏描述的要長得多——允許普通類同時聲明構造函數和解構模式,以及靜態工廠和靜態模式(例如,case Optional.of(var contents))。總之,我們希望這將引領一個更加“對稱”的API設計時代。在這個時代,我們可以輕鬆地拆解對象(當然,只在我們想要這樣做的時候),就像構造對象一樣簡單。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"沒有被採納的選項"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"長期以來,編譯器一直被要求能夠根據過去的條件推斷出細化的類型(通常稱爲流類型)。例如,如果我們有一個以x instanceof Foo爲條件的if語句,編譯器可以推斷出:在if語句體中,類型可以細化爲X&Foo交集類型(其中X是x的靜態類型)。這樣也可以消除強制類型轉換,但我們爲什麼不這樣做呢?簡單地說是因爲這個特性不夠強大。流類型解決了這個特定的問題,但提供了非常低的回報,因爲它的結果只是擺脫instanceof之後的類型轉換,並沒有提供更豐富的switch、解構或更好的API(就語言特性而言,它更像是一個“創可貼”,而不是一種真正的增強)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似地,另一個長期存在的請求是“類型switch”,也就是可以switch目標對象類型,而不僅僅是常量值。同樣,這也提供了一個切實的好處——將一些“if-else”換爲switch——但同樣,這在整體上給語言帶來的改進非常小。模式匹配給我們帶來了這些好處——但還遠遠不止這些。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模式匹配是一種強大的特性,將在Java的幾個版本中發揮重要作用。在第一階段,我們可以在instanceof中使用類型模式,以此來減少儀式代碼。在未來,我們可以在switch中使用模式,可以使用Record的解構模式,可以像構造對象一樣簡便地解構對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者簡介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Brian Goetz是Oracle的Java語言架構師,也是JSR-335(Java編程語言的Lambda表達式)規範負責人。他是暢銷書《Java併發實踐》(Java Concurrency in Practice)的作者。從吉米·卡特擔任美國總統以來,他一直着迷於編程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接"},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/articles\/java-pattern-matching\/","title":"","type":null},"content":[{"type":"text","text":"Java Feature Spotlight: Pattern Matching"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章