Vavr入门(1)

发表于2019-05-10,长度4572, 503个单词, 13分钟读完
Flag Counter

Vavr是一套Java增强集,不同于广泛使用的工具集,它更像Java8以前的JodaTime。

注意:Vavr目前尚无稳定正式版,不同版本间API差别较大

Vavr提供的工具分为三块:元组io.vavr.Tuple,可迭代处理值Value,Lambda表达式函数Function。

元组Tuple和函数表达式Function已经从1.0版本移除,所以如果想使用它们只能选择0.10版本了。

以下测试代码用到了assertj的断言api

元组Tuple

元组是什么东西呢?元组是多个值的集合,不过和数组还有列表不同的是,元组内的元素类型可以各异。另外一点就是元组是不可变的,元组一但生成,里面的每个元素都不能重新赋值,元组的大小也不能改变。 Java没有提供类似元素的功能:当一个方法返回多个值的时候,需要构建一个类,将每个值当成一个实例属性。

Vavr的Tuple是一个接口,目前提供了8个实现类:Tuple1、 Tuple2、 Tuple3到Tuple8,后缀代表了每种元组能包含的元素个数。所以能够看出来,一个元组是没法转换成另一个元组类型的。要访问元组里面的成员,不能通过getter方法,Vavr没提供。成员是public的,直接访问即可。成员名字是下划线(_)拼接上索引值(1~8)。

新建元组

这里创建一个两个成员的元组,分别是字符串和整形:

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

// "Java"
String s = java8._1; 

// 8
Integer i = java8._2; 

元组的创建只能通过工厂方法Tuple.of()。

对元组每个成员分别处理

这里需要用到元组的实例方法map(),处理后会生成新元组:

// (vavr, 1)
Tuple2<String, Integer> that = java8.map(
        s -> s.substring(2) + "vr",
        i -> i / 8
);

对整个元组用一个mapper处理

除了对元组每个成员分别处理外,还可以使用单个映射方法处理:

// (vavr, 1)
Tuple2<String, Integer> that = java8.map(
        (s, i) -> Tuple.of(s.substring(2) + "vr", i / 8)
);

元组与其他类型的转换

将元组转换为其他已知类型使用apply方法:

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

元组说完了,下面说一下Lambda增强类Function。

函数Function

Java8提供了单参数和双参数的函数定义Function和BiFunction。Vavr将函数的参数个数提升到了最多8个,分别是Function0、 Function1、 Function2到Function8。对应的需要抛出受检异常的分别是CheckedFunction1、 CheckedFunction2等等。 看一个例子:

// sum.apply(1, 2) = 3
Function2<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;
    }
};

也可以使用工厂方法of(…​)通过方法引用创建:

Function3<String, String, String, String> function3 =
        Function3.of(this::methodWhichAccepts3Parameters);

Vavr的函数式接口是Java 8 函数编程的增强,它们还提供了下面几种能力:

组合

高一数学的组合函数还记得吧:f(g(x))。Vavr 的组合函数使用andThen或compose:

Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;

Function1<Integer, Integer> add1AndMultiplyBy2 = plusOne.andThen(multiplyByTwo);

then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);
Function1<Integer, Integer> add1AndMultiplyBy2 = multiplyByTwo.compose(plusOne);

then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);

提升

提升针对的是数学上的部分函数:如果一个函数的定义域是X,另一个函数跟他定义一样,只是定义域变成了它的子集,就说第一个函数是全函数,第二个是部分函数。函数的提升会返回当前函数的全函数,返回类型是Option:

比如整除函数:

Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;

当除数是0时会抛异常。这里将它提升为接受全部整数:

Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);

// = None
Option<Integer> i1 = safeDivide.apply(1, 0); 

// = Some(2)
Option<Integer> i2 = safeDivide.apply(4, 2); 

可以看到,提升后的方法在输入原函数的非法值时返回None而不是抛异常,其他时候返回Some。

下面是一个自定义的部分函数,它只能接受正整数:

int sum(int first, int second) {
    if (first < 0 || second < 0) {
        throw new IllegalArgumentException("Only positive integers are allowed"); 
    }
    return first + second;
}

可以通过方法引用直接提升它:

Function2<Integer, Integer, Option<Integer>> sum = Function2.lift(this::sum);

// = None
Option<Integer> optionalResult = sum.apply(-1, 2); 

部分应用

部分应用(Partial application)允许通过固定一个函数的前几个参数值来生成新函数,新函数的参数个数是原函数的参数个数减去固定的参数个数。

Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
Function1<Integer, Integer> add2 = sum.apply(2); 

then(add2.apply(4)).isEqualTo(6);

上面第一个参数被固定为2。看一个复杂点的:

Function5<Integer, Integer, Integer, Integer, Integer, Integer> sum = (a, b, c, d, e) -> a + b + c + d + e;
Function2<Integer, Integer, Integer> add6 = sum.apply(2, 3, 1); 

then(add6.apply(4, 3)).isEqualTo(13);

通过固定Function5的前三个参数生成了Function2对象。

Currying

Currying(我也不知道咋翻译)是通过固定一个参数来生成一个返回Function1的Function1函数。

当作用对象是Function2时,效果和部分应用一样,都返回Function1:

Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
Function1<Integer, Integer> add2 = sum.curried().apply(2); 

then(add2.apply(4)).isEqualTo(6);

当参数多了的时候,他们的区别就明显了。

Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
final Function1<Integer, Function1<Integer, Integer>> add2 = sum.curried().apply(2);

then(add2.apply(4).apply(3)).isEqualTo(9); 

看见差别了吧。我也不知道该咋说,不过你对比一下就能看出和部分应用的差别。现在每次调用apply都返回一个Function1。

记忆

函数记忆是一种缓存技术,记忆函数只会执行一次,以后就直接走缓存了。

下面的例子把一个随机数缓存了起来:

Function0<Double> hashCache = Function0.of(Math::random).memoized();

double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();

then(randomValue1).isEqualTo(randomValue2);

Vavr的Value还没讲,不过这篇文章内容已经不少了。先消化一下,Value单独在下一篇介绍。

但是我不得不提醒一句,这篇文章里的概念都已经过时。Vavr1.0开始这些东西都被移除了,原因是使用场景不够广泛

Written on May 10, 2019
分类: dev, 标签: java vavr
如果你喜欢,请赞赏! davelet