Vavr - Java8函数式编程拓展库使用指南

2年前 (2022) 程序员胖胖胖虎阿
173 0 0

Vavr - Java8函数式编程拓展库使用指南目录

  • Maven 项目包管理
  • Vavr 常用对象讲解
    • 1. Tuples 元组
      • 1.1 Create a tuple 创建一个元组
      • 1.2 Map a tuple 改变元组内部元素值
      • 1.3 Transform a tuple 元组转换
    • 2. Functions 方法
      • 2.1 Composition 方法组合
      • 2.2 Lifting 返回值包装
      • 2.3 Partial application 分部应用
      • 2.4 Currying 颗粒性
      • 2.5 Memoization 内存存储
    • 3. Values 值包装
      • 3.1 Option 选项
      • 3.2 Try 尝试执行
      • 3.3 Lazy 惰性加载
      • 3.4 Either 两者之一
      • 3.5 Future 将来
      • 3.6 Validation 校验
    • 4. Collections 集合包装
    • 5. Pattern Matching 模式匹配
      • 5.1 User-Defined Patterns 自定义匹配条件

Vavr是对Java8函数式编程的拓展,提供更丰富的API及更友好的处理思路方式,使我们在使用函数式变成时更加得心应手。

Maven 项目包管理

`<dependency>
	<groupId>io.vavr</groupId>
 	 <artifactId>vavr</artifactId>
  	<version>0.10.4</version>
</dependency>`

Vavr官方文档地址:vavr官方文档
下面我们会对Vavr的常用对象、功能及基本使用做一个讲解。

Vavr 常用对象讲解

1. Tuples 元组

元组是将多个不同类型的数据包裹的一个容器,比如可以将字符串,数字,数组用一个元组包裹,这样即可以通过一个元组变量获取到包括的任一数据。

1.1 Create a tuple 创建一个元组

Tuple2<String, Integer> java8 = Tuple.of("Java", 8); 
String s = java8._1;   // s = Java
Integer i = java8._2;  // i = 8

1.2 Map a tuple 改变元组内部元素值

/**
 * 这里有两种方式
 * 方式一:map后多个处理函数
 * 方式二:map后一个处理函数
 */
Tuple2<String, Integer> that1 = java8.map(
        s -> s.substring(2) + "vr",
        i -> i / 8
);

Tuple2<String, Integer> that2 = java8.map(
        (s, i) -> Tuple.of(s.substring(2) + "vr", i / 8)
);
// 此时that1、that2里 s = vavr, i = 1

1.3 Transform a tuple 元组转换

String that = java8.apply(
        (s, i) -> s.substring(2) + "vr " + i / 8
// 此时that为 vavr 1

2. Functions 方法

这里的Functions类似Java8里的Function,可以说是对Java8Function做的拓展,其中最大的拓展是Java8中的Function只支持定义一个参数和一个结果,但实际运用当中的话我们得有多个参数,那这样Java8的Function中必须得将参数包装成一个Bean,在Bean中去封装多个参数值,而Vavr则直接将其拓展,使Function可以支持多个参数,最多支持8个参数,创建示例如下:

// 创建方式一
Function<Integer, Integer, Integer> sum = (a, b) -> a + b;

// 创建方式二
Function2<Integer, Integer, Integer> sum = new Function2<Integer, Integer, Integer>() {
    @Override
    public Integer apply(Integer a, Integer b) {
        return a + b;
    }
};

// 创建方式三
Function3<Integer, Integer, Integer> sum = Function3.of((a, b) -> a + b);

2.1 Composition 方法组合

组合前:

Function1<Integer, Integer> func1 = a -> a + 1;
Function1<Integer, Integer> func2 = a -> a * 2;
Function1<Integer, Integer> func3 = func1.andThen(func2);
System.out.println(func3.apply(2));  // (2 + 1) * 3 = 6

组合后:

Function1<Integer, Integer> func1 = a -> a + 1;
Function1<Integer, Integer> func2 = a -> a * 2;
Function1<Integer, Integer> func3 = func2.compose(func1);
System.out.println(func3.apply(2));  // (2 + 1) * 3 = 6

这里需要注意的是,组合是只有Function1才有的方法,因为Function1继承自Function,而Function中就有compose方法,compose方法是先执行参数里的方法,即func1,将返回值作为func2的参数。

2.2 Lifting 返回值包装

返回值包装即返回Option包装对象

Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Function2<Integer, Integer, Option<Integer>> safeDivide = 
  Function2.lift(divide);
Option<Integer> i2 = safeDivide.apply(4, 2);

2.3 Partial application 分部应用

Function2<Integer, Integer, Integer> func1 = (a, b) -> a + b;
Function1<Integer, Integer> func2 = func1.apply(2);
System.out.println(func2.apply(4)) // 6

// func2等同如下写法
Function2<Integer, Integer, Integer> func1 = (a, b) -> a + b;
Function1<Integer, Integer> func2 = (b) -> 2 + b;
System.out.println(func2.apply(4)) // 6

2.4 Currying 颗粒性

Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
final Function1<Integer, Function1<Integer, Integer>> add2 = 
  sum.curried().apply(2);
System.out.println(add2.apply(4));   // Function3
System.out.println(add2.apply(4).apply(3))  // 9

2.5 Memoization 内存存储

Function0<Double> hashCache =
        Function0.of(Math::random).memoized();
double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();
// 这里两个输出的值一致,randomValue1时产生之后就已缓存,第二次调用直接获取缓存值
System.out.println(randomValue1);
System.out.println(randomValue2);

3. Values 值包装

3.1 Option 选项

Option是基于Java8 Optional的拓展,其主要目的是为了补充解决Optional在使用时的诸多不便,首先Option与Optional最大不同之处在于,Option是可以接收null值,并且会将null作为一个真实的值处理,比如下方代码:

Optional<String> op1 = Optional.of("")
  .map(s -> (String) null)  // 先转成null,转成null后会直接断开,不在执行
  .map(s -> s = "abc");     // 这里将不会执行

Option<String> op2 = Option.of("")
  .map(s -> (String) null)  // 先转成null
  .map(s -> s = "abc");     // 再转成abc
 
System.out.println(op2.get());  // 输出abc
System.out.println(op1.get());  // 报错:No value present

从上方的例子我们可以看出,Optional在使用时,当包裹的值为null值时,会立即断开,后面的内容都不再执行,在实际编码时这种方式不太友好,所以Option在这个基础上做了改善,把null值也做为一个值来处理。同时两者在表现null结果的处理上也不太一致,如下方代码:

Optional<String> op1 = Optional.of("").map(s -> (String) null);
Option<String> op2 = Option.of("").map(s -> (String) null);
System.out.println(op1.orElse("i am null"));     // 输出 i am null
System.out.println(op2.getOrElse("i am null"));  // 输出 null

这里会发现,哪怕Option中包裹的值为null,在执行orElse()方法时也不会走里面的内容,
而是直接get()->null,那如何能让orElse生效?

Option<String> op2 = Optional.of("")
  .map(s -> (String) null)
  .filter(Objects::nonNull);
System.out.println(op2.orElse("i am null"));     // 输出 i am null

通过以上代码也就意味着Option必须是通过filter过滤,未产生匹配结果,最终才会致使Option中没有包裹值,此时才会做空处理。这也是Option与Optional最大区别与优化。

其次,Option在Optional的基础上拓展及修改了更丰富的API,让我们可以更为方便的使用,具体提供的API可从具体调用时了解。

3.2 Try 尝试执行

Try同Option一样,同样都是一个包裹值的容器,区别是Try能处理异常。在Java代码里,很多方法都会抛出异常让上层处理,而在函数式编程,调用这种方法时,则需要通过Try-Catch进行异常处理,这样会破坏函数式编程的可阅读性和美观度,如下方代码:

Option<Field> field = Option.of(UserInfo.class)
  .map(clz -> {
    try {
      return clz.getDeclaredField("id");
    } catch (NoSuchFieldException e) {
      return null;
    }
  });

这种代码严重破坏了函数式编程代码结构,此时我们可以通过Try来进行改造,如下:

Try<Field> field2 = Try.success(UserInfo.class)
	.mapTry(clz -> clz.getDeclaredField("id"));

Try<Field> field3 = Try.of(() -> UserInfo.class.getDeclaredField("id"));

Try field2 = Try.success(UserInfo.class)
.mapTry(clz -> clz.getDeclaredField(“id”));

Try field3 = Try.of(() -> UserInfo.class.getDeclaredField(“id”));
以上两种方式都可以将异常捕获完美解决,如果Try包裹的内容发生异常了会怎么办,当前代码肯定是会报错而停止执行,在实际的业务场景中为了不终止程序的运行必定是需要对异常进行特殊处理,Try也就有了更多的作用,代码如下:

Try<Field> field2 = Try.success(UserInfo.class)
  .mapTry(clz -> clz.getDeclaredField("aaa"))
  .recover(e -> null) // 这里只是简单演示效果,直接产生null,也可做业务代码
  .filter(Objects::nonNull);
System.out.println(field2.getOrElse(() -> {
  System.out.println("orElse");
  return null;
}));
// 输出 orElse
// 输出 null

3.3 Lazy 惰性加载

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)

3.4 Either 两者之一

Either表示可能有两种不同类型的值,分别称为左值和右值,只能是其中一种情况,Either通常用来表示成功或者失败两种情况,惯例是把成功的值作为右值,失败的值作为左值。可以在Either后添加应用于左值或又值的计算,当中在实际运行中只会产生左值或右值中的其中一种,也就是说添加的计算也只有一个会生效。

// 大于0,返回成功(右值),小于等于0,返回失败(左值)
Function<Integer, Either<Integer, Integer>> func =
  i -> i > 0 ? Either.right(i)
  : Either.left(-100);

Either<Integer, Integer> either = func.apply(0);
System.out.println(either.mapLeft(l -> l * 100).getLeft());  // -10000

Either<Integer, Integer> either1 = func.apply(100);
System.out.println(either1.map(r -> r * 100).get());         // 10000

3.5 Future 将来

Vavr通过Future简化了线程的使用方式,不用再像Java定义任务,创建线程,再执行,直接创建一个Future对象即可。Future提供的所有操作都是非阻塞的,其底层的ExecutorService用于执行异步处理程序。代码如下:

System.out.println("当前线程名称:" + Thread.currentThread().getName());
        Integer result = Future.of(() -> {
            System.out.println("future线程名称:" + Thread.currentThread().getName());
            Thread.sleep(2000);
            return 100;
        })
                .map(i -> i * 10)
                .await(3000, TimeUnit.MILLISECONDS)
                .get();
        System.out.println(result);
// 当前线程名称:main
// future线程名称:ForkJoinPool.commonPool-worker-3
// 过三秒输出,1000

3.6 Validation 校验

Validation能够将错误校验进行整合,通常情况下,程序遇到错误就会终止处理,然而,Validation会继续处理,并将程序错误累计,最终成为一个成体处理。代码如下:

Function1<String, Validation<String, String>> validateUserName = (name) ->
  name.replaceAll("[a-zA-Z]", "").isEmpty()
  ? Validation.valid(name)
  : Validation.invalid("用户名验证失败");

Function1<Integer, Validation<String, Integer>> validationSex = (sex) ->
  Objects.nonNull(BaseEnumHelper.valueOf(Sex.class, sex))
  ? Validation.valid(sex)
  : Validation.invalid("未知性别");

Function2<String, Integer, Validation<Seq<String>, String>> validationUser = (userName, sex) ->
  Validation.combine(
  validateUserName.apply(userName),
  validationSex.apply(sex)
).ap((a, b) -> "success");

// 输出success
System.out.println(validationUser.apply("JohnDoe", 1).get());
// 输出List(用户名验证失败, 未知性别)
System.out.println(validationUser.apply("John?Doe!4", -1).getError());

4. Collections 集合包装

Vavr为了保证函数式编程的快捷性,将常用的Java对象都进行了包装,使其支持函数式编程。下面以List举例:

// Java8
Arrays.asList(1, 2, 3).stream().reduce((i, j) -> i + j);
IntStream.of(1, 2, 3).sum();

// Vavr - io.vavr.collection.List
List.of(1, 2, 3).sum();

除此之外还有Stream,同Java8 Stream,API比Java8Stream更丰富,详见API:Vavr对Java常用类型封装清单。

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

5. Pattern Matching 模式匹配

Java的switch case是具有很大的局限性的,仅仅对简单的基本类型进行了支持,而对于包装类型是无法使用的。而在Scala中是支持各种类型的模式匹配,不仅如此,其还具有对象解构、前置条件,守卫,而vavr支持了这些功能。

vavr中用$表达不同的匹配模式。
● $() -通配符模式
● $(value) -等于模式
● $(predicate) -条件模式

代码示例:

int i = 1;
String a = Match(i).of(
  Case($(1), "one"), // 等值匹配
  Case($(2), "two"),
  Case($(), "?")
);

String b = Match(i).of(
  Case($(is(1)), "one"), // 条件匹配
  Case($(is(2)), "two"),
  Case($(), "?")
);

5.1 User-Defined Patterns 自定义匹配条件

import io.vavr.match.annotation.*;

@Patterns
class My {

    @Unapply
    static <T> Tuple1<T> Optional(java.util.Optional<T> optional) {
        return Tuple.of(optional.orElse(null));
    }
}
Match(optional).of(
    Case($Optional($(v -> v != null)), "defined"),
    Case($Optional($(v -> v == null)), "empty")
);

版权声明:程序员胖胖胖虎阿 发表于 2022年9月19日 上午4:32。
转载请注明:Vavr - Java8函数式编程拓展库使用指南 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...