Vavr入门(1)
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开始这些东西都被移除了,原因是使用场景不够广泛
