衆所周知, Java8 在必定程度上支持了函數式編程,但標準庫提供的函數式 API 不是很完備和友好。java
爲了更好的進行函數式編程,咱們就不得不借助於第三方庫,而 VAVR 就是這方面的佼佼者,它能夠有效減小代碼量並提升代碼質量。git
VAVR 可不是默默無聞之輩,它的前身是發佈於 2014 年的 Javaslang,目前在 github 上有着近 4k 的 star。github
看到這兒,不少人就說我標題黨了,一個 Java 庫還來顛覆 Java ?編程
這可不不是我玩震驚體,打開 VAVR 的官網 ,它的首頁就用加粗字體寫着 「vavr - turns java™ upside down」數組
這翻譯過來不就是顛覆 Java 嗎?緩存
閱讀本文須要讀者對 Java8 的 lambda 語法和經常使用 API 有必定的瞭解。安全
因爲是一篇框架的介紹文(地推 ing),爲了不寫成官方文檔的翻譯,本文會有一些約束markdown
關於示例代碼,基本會以單元測試的形式給出並保證運行經過數據結構
注:本文使用的 VAVR 版本爲 0.10.3,JDK 版本爲 11。app
先來個概覽
不得不說 Java8 的集合庫引入 Stream 之後確實很好用,但也正是由於使用了 Stream,不得不寫不少樣板代碼,反而下降了很多體驗。
// of 方法是 Java9 開始提供的靜態工廠 java.util.List.of(1, 2, 3, 4, 5) .stream() .filter(i -> i > 3) .map(i -> i * 2) .collect(Collectors.toList()); 複製代碼
並且 Java 的集合庫自己是可變的,顯然違背了函數式編程的基本特性 - 不可變,爲此 VAVR 設計了一套全新的集合庫,使用體驗無限接近於 Scala。
更簡潔的 API
io.vavr.collection.List.of(1, 2, 3, 4, 5) .filter(i -> i > 3) .map(i -> i * 2); 複製代碼
往集合追加數據會產生新的集合,從而保證不可變
var list = io.vavr.collection.List.of(1, 2) var list2 = list .append(List.of(3, 4)) .append(List.of(5, 6)) .append(7); // list = [1, 2] // list2 = [1, 2, 3, 4, 5, 6] 複製代碼
強大的兼容性,能夠很是方便的與 Java 標準集合庫進行轉換
var javaList = java.util.List.of(1, 2, 3); java.util.List<Integer> javaList2 = io.vavr.collection.List.ofAll(javaList) .filter(i -> i > 1) .map(i -> i * 2) .toJavaList(); 複製代碼
再來看一個稍微複雜一點的例子:過濾一批用戶中已成年的數據,按照年齡對其分組,每一個分組只展現用戶的姓名。
/** * 用戶信息 */ @Data class User { private Long id; private String name; private Integer age; } 複製代碼
先用 Java 標準集合庫來實現這個需求,能夠看見 collect(...)
這一長串嵌套是真的很難受
public Map<Integer, List<String>> userStatistic(List<User> users) { return users.stream() .filter(u -> u.getAge() >= 18) .collect(Collectors.groupingBy(User::getAge, Collectors.mapping(User::getName, Collectors.toList()))); } 複製代碼
再來看看 VAVR 的實現,是否是更簡潔,更直觀?
public Map<Integer, List<String>> userStatistic(List<User> users) { return users.filter(u -> u.getAge() >= 18) .groupBy(User::getAge) .mapValues(usersGroup -> usersGroup.map(User::getName)); } 複製代碼
VAVR 的集合庫提供了更多 Functional 的 API,好比
雖然代碼實例都是用的 List,可是以上特性在 Queue、Set、Map 均可以使用,都支持與 Java 標準庫的轉換。
熟悉 Haskell、Scala 的同窗確定對「元組」這個數據結構不陌生。
元組相似一個數組,能夠存放不一樣類型的對象並維持其類型信息,這樣在取值時就不用 cast 了。
// scala 的元組,用括號構建 val tup = (1, "ok", true) // 按索引取值,執行對應類型的操做 val sum = tup._1 + 2 // int 加法 val world = "hello "+tup._2 // 字符串拼接 val res = !tup._3 // 布爾取反 複製代碼
固然,Java 並無原生的語法支持建立元組,標準庫也沒有元組相關的類。
不過,VAVR 經過泛型實現了元組,經過 Tuple
的靜態工廠,咱們能夠很是輕易的建立元組( 配合 Java10 的 var 語法簡直不要太美好)
import io.vavr.Tuple; public TupleTest { @Test public void testTuple() { // 一元組 var oneTuple = Tuple.of("string"); String oneTuple_1 = oneTuple._1; // 二元組 var twoTuple = Tuple.of("string", 1); String twoTuple_1 = twoTuple._1; Integer twoTuple_2 = twoTuple._2; // 五元組 var threeTuple = Tuple.of("string", 2, 1.2F, 2.4D, 'c'); String threeTuple_1 = threeTuple._1; Integer threeTuple_2 = threeTuple._2; Float threeTuple_3 = threeTuple._3; Double threeTuple_4 = threeTuple._4; Character threeTuple_5 = threeTuple._5; } } 複製代碼
若是沒有 var
,就得寫出下面這樣冗長的變量定義
Tuple5<String, Integer, Float, Double, Character> tuple5 = Tuple.of("string", 2, 1.2F, 2.4D, 'c'); 複製代碼
目前,VAVR 最多支持構造八元組,也就是支持最多 8 個類型,而不是最多 8 個值。
當元組和「模式匹配」的配合使用時,那更是強大的一塌糊塗
PS:雖然如今提模式匹配有點早了(後面會再碰見的),不過咱們仍然能夠提早感覺一下
var tup = Tuple.of("hello", 1); // 模式匹配 Match(tup).of( Case($Tuple2($(is("hello")), $(is(1))), (t1, t2) -> run(() -> {})), Case($Tuple2($(), $()),(t1, t2) ->run(() -> {})) ); 複製代碼
上面的代碼其實就等同於 if...else
// 等同於 if...else if (tup._1.equalas("hello") && tup._2 == 1) { // ... do something } else { // ... do something } 複製代碼
Java8 引入了 Optional 去解決臭名昭著的 NullPointerException,而 VAVR 也有一個相似的工具 - Option,但它卻有着不一樣的設計。
除了 Option 外,VAVR 還實現了 Try、Either、Future 等函數式的結構,它們都是 Java 標準庫沒有但很是強大的工具。
Option
與 Java 標準庫的 Optional
很類似,都表明着一個可選值,可是二者的設計倒是大不相同的。(VAVR 的 Option 設計和 Scala 更接近)
在 VAVR 中,Option
是一個 interface,它的具體實現有 Some 和 None
你能夠經過下面的單元測試進行驗證
@Test public void testOption() { // 經過 of 工廠方法構造 Assert.assertTrue(Option.of(null) instanceof Option.None); Assert.assertTrue(Option.of(1) instanceof Option.Some); // 經過 none 或 some 構造 Assert.assertTrue(Option.none() instanceof Option.Some); Assert.assertTrue(Option.some(1) instanceof Option.Some); } 複製代碼
而對於 java.util.Optional
來講,不管經過什麼方式構造,都是同一個類型。
@Test public void testOptional() { Assert.assertTrue(Optional.ofNullable(null) instanceof Optional); Assert.assertTrue(Optional.of(1) instanceof Optional); Assert.assertTrue(Optional.empty() instanceof Optional); Assert.assertTrue(Optional.ofNullable(1) instanceof Optional); } 複製代碼
爲何二者會有這樣的設計區別呢?
本質上來說就是對 「Option 的做用就是使得對 null 的計算保證安全嗎?」這一問題的不一樣回答。
下面的的兩個測試方法,一樣的邏輯,用 Option 和 Optional 卻得出了不一樣的結果
@Test public void testWithJavaOptional() { // Java Optional var result = Optional.of("hello") .map(str -> (String) null) .orElseGet(() -> "world"); // result = "world" Assert.assertEquals("word", result); } @Test public void testWithVavrOption() { // Vavr Option var result = Option.of("hello") .map(str -> (String) null) .getOrElse(() -> "world"); // result = null Assert.assertNull(result); } 複製代碼
在 VAVR 的測試代碼中,經過 Optional.of("hello")
實際上獲得了一個 Some("hello")
對象。
隨後調用 map(str -> (String)null)
返回的仍然是一個 Some
對象(Some 表明有值),因此最終的 result = null,而不是 getOrElse(() -> "world")
返回的 world 字符串。
在 Java 的測試代碼中,調用 map(str -> null)
時,Optional 就已經被切換爲了 Optional.empty,因此最終就返回了 orElseGet(() -> "world")
的結果。
這也是函數式開發者們批判 java.util.Optional 設計的一個點
除了設計上的區別外, io.vavr.control.Option 比 java.util.Optional 也要多出更多友好的 API
@Test public void testVavrOption() { // option 直接轉爲 List List<String> result = Option.of("vavr hello world") .map(String::toUpperCase) .toJavaList(); Assert.assertNotNull(result); Assert.assertEquals(1, result.size()); Assert.assertEquals("vavr hello world", result.iterator().next()); // exists(Function) boolean exists = Option.of("ok").exists(str -> str.equals("ok")); Assert.assertTrue(exists); // contains boolean contains = Option.of("ok").contains("ok"); Assert.assertTrue(contains); } 複製代碼
考慮到與標準庫的兼容,Option 能夠很方便的與 Optional 進行互轉
Option.of("toJava").toJavaOptional(); Option.ofOptional(Optional.empty()); 複製代碼
Try 和 Option 相似,也相似於一個「容器」,只不過它容納的是可能出錯的行爲,你是否是立刻就想到了 try..catch 結構?
try { //.. } catch (Throwable t) { //... } finally { //.... } 複製代碼
經過 VAVR 的 Try,也能實現另一種更 functional 的 try...catch。
/** * 輸出 * failure: / by zero * finally */ Try.of(() -> 1 / 0) .andThen(r -> System.out.println("and then " + r)) .onFailure(error -> System.out.println("failure" + error.getMessage())) .andFinally(() -> { System.out.println("finally"); }); 複製代碼
Try 也是個接口, 具體的實現是 Success 或 Failure
和 Optoin 同樣,也能夠經過 of 工廠方法進行構建
@Test public void testTryInstance() { // 除以 0 ,構建出 Failure var error = Try.of(() -> 0 / 0); Assert.assertTrue(error instanceof Try.Failure); // 合法的加法,構建出 Success var normal = Try.of(() -> 1 + 1); Assert.assertTrue(normal instanceof Try.Success); } 複製代碼
經過 Try 的 recoverWith 方法,咱們能夠很優雅的實現降級策略
@Test public void testTryWithRecover() { Assert.assertEquals("NPE", testTryWithRecover(new NullPointerException())); Assert.assertEquals("IllegalState", testTryWithRecover(new IllegalStateException())); Assert.assertEquals("Unknown", testTryWithRecover(new RuntimeException())); } private String testTryWithRecover(Exception e) { return (String) Try.of(() -> { throw e; }) .recoverWith(NullPointerException.class, Try.of(() -> "NPE")) .recoverWith(IllegalStateException.class, Try.of(() -> "IllegalState")) .recoverWith(RuntimeException.class, Try.of(() -> "Unknown")) .get(); } 複製代碼
對於 Try 的計算結果,能夠經過 map 進行轉換,也能夠很方便的與 Option 進行轉換。
還可使用 map 對結果進行轉換,而且與 Option 進行交互
@Test public void testTryMap() { String res = Try.of(() -> "hello world") .map(String::toUpperCase) .toOption() .getOrElse(() -> "default"); Assert.assertEquals("HELLO WORLD", res); } 複製代碼
這個 Future 可不是 java.util.concurrent.Future
,但它們都是對異步計算結果的一個抽象。
vavr 的 Future
提供了比 java.util.concurrent.Future
更友好的回調機制
@Test public void testFutureFailure() { final var word = "hello world"; io.vavr.concurrent.Future .of(Executors.newFixedThreadPool(1), () -> word) .onFailure(throwable -> Assert.fail("不該該走到 failure 分支")) .onSuccess(result -> Assert.assertEquals(word, result)); } @Test public void testFutureSuccess() { io.vavr.concurrent.Future .of(Executors.newFixedThreadPool(1), () -> { throw new RuntimeException(); }) .onFailure(throwable -> Assert.assertTrue(throwable instanceof RuntimeException)) .onSuccess(result -> Assert.fail("不該該走到 success 分支")); } 複製代碼
它也能夠和 Java 的 CompleableFuture 互轉
Future.of(Executors.newFixedThreadPool(1), () -> "toJava").toCompletableFuture(); Future.fromCompletableFuture(CompletableFuture.runAsync(() -> {})); 複製代碼
最後再來簡單過一下 Either 和 Lazy 吧
Either 它表示某個值可能爲兩種類型中的一種,好比下面的 compute()
函數的 Either 返回值表明結構可能爲 Exception 或 String。
一般用 right 表明正確的值(英文 right 有正確的意思)
public Either<Exception, String> compute() { //... } public void test() { Either<Exception, String> either = compute(); // 異常值 if (either.isLeft()) { Exception exception = compute().getLeft(); throw new RuntimeException(exception); } // 正確值 if (either.isRight()) { String result = compute().get(); // ... } } 複製代碼
Lazy 也是一個容器,他能夠延遲某個計算,直到該計算被首次調用,初次調用以後該結果會被緩存,後續調用就能夠直接拿到結果。
Lazy<Double> lazy = Lazy.of(Math::random); lazy.isEvaluated(); // = false lazy.get(); // = 0.123 (random generated) lazy.isEvaluated(); // = true lazy.get(); // = 0.123 (memoized) 複製代碼
在 io.vavr.API
中提供了不少靜態方法來模擬 Scala 的語法構造 Option、Try 這些結構,可是要結合 Java 的靜態導入使用
import static io.vavr.API.*; @Test public void testAPI() { // 構造 Option var some = Some(1); var none = None(); // 構造 Future var future = Future(() -> "ok"); // 構造 Try var tryInit = Try(() -> "ok"); } 複製代碼
固然這個大寫字母開頭的函數名有點不符合 Java 的方法命名規範,算是一種 Hack 手段吧。
關於更多細節的內容,有興趣的能夠去查閱官網文檔學習
這裏的模式指的是數據結構的組成模式,在 Scala 中能夠直接經過 match 關鍵字使用模式匹配
def testPatternMatch(nameOpt: Option[String], nums: List[Int]) = { /** * 匹配 Option 的結構 */ nameOpt match { case Some(name) => println(s"你好,$name") case None => println("無名之輩") } /** * 匹配 List 的結構 */ nums match { case Nil => println("空列表") case List(v) => println(s"size=1 $v") case List(v, v2) => println(s"size=2 $v、 $v2") case _ => println("size > 2") } } 複製代碼
在 Java 中沒有模式匹配的概念,天然就沒有相關的語法了(switch 可不算)。
不過 VAVR 使用 OOP 的方式實現了了模式匹配,雖然比不了 Scala 原生的體驗,但也至關接近了
Java 在 JEP 375: Pattern Matching for instanceof 提案中針對 instanceof 實現了一個模式匹配的特性(預計在 Java15 發佈),不過我以爲該特性距離 Scala 的模式匹配還有一段距離
咱們來實現一個將 BMI 值格式化成文字描述的需求,先用 Java 的命令式風格來實現
public String bmiFormat(double height, double weight) { double bmi = weight / (height * height); String desc; if (bmi < 18.5) { desc = "有些許晃盪!"; } else if (bmi < 25) { desc = "繼續加油哦!"; } else if (bmi < 30) { desc = "你是真的穩!"; } else { desc = "難受!"; } return desc; } 複製代碼
接下來再用 VAVR 的模式匹配來重構吧,消滅這些 if..else。
爲了讓語法體驗更友好,最好先經過 static import
導入 API。
import static io.vavr.API.*; 複製代碼
下面是重構後的代碼段
public String bmiFormat(double height, double weight) { double bmi = weight / (height * height); return Match(bmi).of( // else if (bmi < 18.5) Case($(v -> v < 18.5), () -> "有些許晃盪!"), // else if (bmi < 25) Case($(v -> v < 25), () -> "繼續加油哦!"), // else if (bmi < 30) Case($(v -> v < 30), () -> "你是真的穩!"), // else Case($(), () -> "難受!") ); } 複製代碼
Match(...),Case(...),$(...) 都是 io.vavr.API
的靜態方法,用於模擬「模式匹配」的語法
最後一個 $() 表示匹配除了上面以外的全部狀況
爲了便於讀者理解,我將各個方法的簽名簡單列了一下(Case 和 $ 方法有不少重載,就不全列了)
public static <T> Match<T> Match(T value) {...} public static <T, R> Case<T, R> Case(Pattern0<T> pattern, Function<? super T, ? extends R> f) {...} public static <T> Pattern0<T> $(Predicate<? super T> predicate) {...} 複製代碼
of
是 Match 對象的方法
public final <R> R of(Case<? extends T, ? extends R>... cases) {...} 複製代碼
來,再展現一下自創的語法記憶
匹配一下(這個東西)的結構,是否是下面的狀況之一 // Match(XXX).Of( - 結構和 A 同樣,作點什麼事情 //Case( $(A), () -> doSomethingA() ), - 結構和 B 同樣,作點什麼事情 //Case( $(B), () -> doSomethingB() ), - ..... - 和上面的結構都不同,也作點事情 //Case( $(), () -> doSomethingOthers()) //); 複製代碼
當模式匹配和前面提到的 Option、Try、Either、Tuple 結合時,那但是 1 + 1 > 3 的結合。
下面的代碼展現了「模式匹配」是如何讓 Option 如虎添翼的
import static io.vavr.API.*; import static io.vavr.Patterns.$None; import static io.vavr.Patterns.$Some; public class PatternMatchTest { @Test public void testMatchNone() { // 匹配 None var noneOpt = Option.none(); Match(noneOpt).of( Case($None(), r -> { Assert.assertEquals(Option.none(), r); return true; }), Case($(), this::failed) ); } @Test public void testMatchValue() { // 匹配某一個值爲 Nice 的 Some var opt2 = Option.of("Nice"); Match(opt2).of( Case($Some($("Nice")), r -> { Assert.assertEquals("Nice", r); return true; }), Case($(), this::failed) ); } @Test public void testMatchAnySome() { // 匹配 Some,值任意 var opt = Option.of("hello world"); Match(opt).of( Case($None(), this::failed), Case($Some($()), r -> { Assert.assertEquals("hello world", r); return true; }) ); } private boolean failed() { Assert.fail("不該該執行該分支"); return false; } } 複製代碼
還有 Try,順便說一句,有時候 Case 沒有返回值的時候, 第二個參數能夠用 API.run() 替代
import static io.vavr.API.*; import static io.vavr.Patterns.*; import static io.vavr.Predicates.instanceOf; public class PatternMatchTest { @Test public void testMatchFailure() { var res = Try.of(() -> { throw new RuntimeException(); }); Match(res).of( // 匹配成功狀況 Case($Success($()), r -> run(Assert::fail)), // 匹配異常爲 RuntimeException Case($Failure($(instanceOf(RuntimeException.class))), r -> true), // 匹配異常爲 IllegalStateException Case($Failure($(instanceOf(IllegalStateException.class))), r -> run(Assert::fail)), // 匹配異常爲 NullPointerException Case($Failure($(instanceOf(NullPointerException.class))), r -> run(Assert::fail)), // 匹配其他失敗的狀況 Case($Failure($()), r -> run(Assert::fail)) ); } @Test public void testMatchSuccess() { var res = Try.of(() -> "Nice"); Match(res).of( // 匹配任意成功的狀況 Case($Success($()), r -> run(() -> Assert.assertEquals("Nice", r))), // 匹配任意失敗的狀況 Case($Failure($()), r -> run(Assert::fail)) ); } } 複製代碼
如今再回頭看看元組的代碼,你能夠嘗試一下本身寫寫三元組的模式匹配了。
本文只介紹了一些經常使用的特性,而除此以外,VAVR 還支持 Curring、Memoization、 Partial application 等高級特性,若是想深刻的學習能夠前往官網瞭解。
最後,這塊磚已經拋出去了,能不能引到你這塊玉呢?
廣告:
若是你正在找基於 Java9+ 的項目用於學習新特性,我自薦一下 PrettyZoo,
這是一款基於 Java11開發的 zookeeper 桌面客戶端,使用了模塊化,var 等諸多新特性,歡迎 star、fork、issue。