VAVR:顛覆你的 Java 體驗

何方神聖?

衆所周知, 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

  • 不會窮盡全部特性和 API,僅作拋磚引玉
  • 不會深刻到源碼細節

關於示例代碼,基本會以單元測試的形式給出並保證運行經過數據結構

注:本文使用的 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,好比

  • take(Integer) 取前 n 個值
  • tail() 取除了頭結點外的集合
  • zipWithIndex() 使得便利時能夠拿到索引(不用 fori)
  • find(Predicate) 基於條件查詢值,在 Java 標準庫得使用 filter + findFirst 才能實現
  • .....

雖然代碼實例都是用的 List,可是以上特性在 Queue、Set、Map 均可以使用,都支持與 Java 標準庫的轉換。

元組,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
}
複製代碼

除了 Option,還有 Try、Either、Future......

Java8 引入了 Optional 去解決臭名昭著的 NullPointerException,而 VAVR 也有一個相似的工具 - Option,但它卻有着不一樣的設計。

除了 Option 外,VAVR 還實現了 Try、Either、Future 等函數式的結構,它們都是 Java 標準庫沒有但很是強大的工具。

Option

Option 與 Java 標準庫的 Optional 很類似,都表明着一個可選值,可是二者的設計倒是大不相同的。(VAVR 的 Option 設計和 Scala 更接近)

在 VAVR 中,Option 是一個 interface,它的具體實現有 Some 和 None

  • 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

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

  • 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

這個 Future 可不是 java.util.concurrent.Future,但它們都是對異步計算結果的一個抽象。

vavr 的 Future 提供了比 java.util.concurrent.Future 更友好的回調機制

  • onFailure 失敗的回調
  • onSuccess 成功的回調
@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 手段吧。

關於更多細節的內容,有興趣的能夠去查閱官網文檔學習

模式匹配:if..else 的剋星

這裏的模式指的是數據結構的組成模式,在 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。

相關文章
相關標籤/搜索