VAVR:颠覆你的 Java 体验

作者 | 田伟然

回首向来萧瑟处,归去,也无风雨也无晴。 

杏仁工程师,关注编码和诗词。

何方神圣?

众所周知, Java8 在一定程度上支持了函数式编程,但标准库提供的函数式 API 不是很完备和友好。

为了更好的进行函数式编程,我们就不得不借助于第三方库,而 VAVR 就是这方面的佼佼者,它可以有效减少代码量并提高代码质量。

VAVR 可不是默默无闻之辈,它的前身是发布于 2014 年的 Javaslang,目前在 github 上有着近 4k 的 star。

看到这儿,很多人就说我标题党了,一个 Java 库还来颠覆 Java ?

这可不不是我玩震惊体,打开 VAVR 的官网 ,它的首页就用加粗字体写着 「vavr - turns java™ upside down」

VAVR:颠覆你的 Java 体验_第1张图片

这翻译过来不就是颠覆 Java 吗?

食用指南

阅读本文需要读者对 Java8 的 lambda 语法和常用 API 有一定的了解。

由于是一篇框架的介绍文(地推 ing),为了避免写成官方文档的翻译,本文会有一些约束

  • 不会穷尽所有特性和 API,仅做抛砖引玉

  • 不会深入到源码细节

关于示例代码,基本会以单元测试的形式给出并保证运行通过

注:本文使用的 VAVR 版本为 0.10.3,JDK 版本为 11。

先来个概览

VAVR:颠覆你的 Java 体验_第2张图片

集合,全新的开始

不得不说 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 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> userStatistic(List users) {
    return users.stream()
      .filter(u -> u.getAge() >= 18)
      .collect(Collectors.groupingBy(User::getAge, Collectors.mapping(User::getName, Collectors.toList())));
}

再来看看 VAVR 的实现,是不是更简洁,更直观?

public Map> userStatistic(List 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 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

VAVR:颠覆你的 Java 体验_第3张图片

  • 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 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:代表执行出现异常

VAVR:颠覆你的 Java 体验_第4张图片

和 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 compute() {
  //...
}

public void test() {
	Either either = compute();
  
  // 异常值
  if (either.isLeft()) {
    Exception exception = compute().getLeft();
    throw new RuntimeException(exception);
  }

  // 正确值
  if (either.isRight()) {
    String result = compute().get();
    // ...
  }
}
  • Lazy 也是一个容器,他可以延迟某个计算,直到该计算被首次调用,初次调用之后该结果会被缓存,后续调用就可以直接拿到结果。

Lazy 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  Match Match(T value) {...}

public static  Case Case(Pattern0 pattern, Function f) {...}

public static  Pattern0 $(Predicate predicate) {...}

of 是 Match 对象的方法

public final  R of(Case... 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 等高级特性,如果想深入的学习可以前往官网了解。

最后,这块砖已经抛出去了,能不能引到你这块玉呢?

全文完


以下文章您可能也会感兴趣:

  • 所谓 Serverless,你理解对了吗?

  • 如何写好产品中的提示文案

  • Web 开发打印总结

  • JVM 揭秘: 一个 class 文件的前世今生

  • Android 图片编辑的原理与实现 -- 涂鸦与马赛克

  • 微服务环境下的集成测试探索(二)—— 契约式测试

  • 微服务环境下的集成测试探索(一) —— 服务 Stub & Mock

  • Objective-C 中的语法糖

  • Facebook、Google、Amazon 是如何高效开会的

  • 谈谈到底什么是抽象,以及软件设计的抽象原则

  • 后端的缓存系统浅谈

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected]

你可能感兴趣的:(java,stream,编程语言,callback,大数据)