Hystrix 是 Netflix 开源的限流、熔断降级组件,去年发现 Hystrix 已经不再更新了,而在 github 主页上将我引导到了另一个替代项目——resilience4j,这个项目是基于 Java 8 开发的,并且只使用了 vavr 库,也就是我们今天要介绍的主角。
Lambda 表达式
既然要谈 vavr,那么先要谈为什么要使用 vavr,vavr 是为了增强 Java 的函数式编程体验的,那么这里先介绍下 Java 中的函数式编程。
Java 8 引入了函数式编程范式,思路是:将函数作为其他函数的参数传递,其实在 Java 8 之前,Java 也支持类似的功能,但是需要使用接口实现多态,或者使用匿名类实现。不管是接口还是匿名类,都有很多模板代码,因此 Java 8 引入了 Lambda 表达式,正式支持函数式编程。
比方说,我们要实现一个比较器来比较两个对象的大小,在 Java 8 之前,只能使用下面的代码:
Compartor<Apple> byWeight = new Comparator<Apple>() {public int compare(Apple a1, Apple a2) {return a1.getWeight().compareTo(a2.getWeight());
}
}
上面的代码使用 Lambda 表达式可以写成下面这样(IDEA 会提示你做代码的简化):
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
关于 Lambda 表达式,你需要掌握的知识点有:
- Lambda 表达式可以理解为是一种匿名函数:它没有名称,但是又参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表;
- 函数式接口 就是仅仅声明了一个抽象方法的接口;
- @FunctionalInterface注解对于函数式接口的作用,类似于 @Override 对于被重写的方法——不是必须的,但是用了有助于提升代码的可读性,因此如果你在开发中自己定义函数式接口,最好也使用这个注解修饰;
- Java 8 自带一些常用的函数式接口,放在
java.util.function
包里,包括 Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T> 和 BinaryOperator<T> 等等;
vavr
受限于 Java 标准库的通用性要求和二进制文件大小,Java 标准库对函数式编程的 API 支持相对比较有限。函数的声明只提供了 Function 和 BiFunction 两种,流上所支持的操作的数量也较少。基于这些原因,你也许需要 vavr
来帮助你更好得使用 Java 8 进行函数式开发。
vavr 是在尝试让 Java 拥有跟 Scala 类似的语法。vavr 提供了不可变的集合框架;更好的函数式编程特性;元组。
集合
Vavr 实现了一套新的 Java 集合框架来匹配函数式编程范式,vavr 提供的集合都是不可变的。在 Java 中使用 Stream,需要显示得将集合转成 steam 的步骤,而在 vavr 中则免去了这样的步骤。
- vavr 的 List 是不可变的链表,在该链表对象上的操作都会生成一个新的链表对象。
使用 Java 8 的代码:
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();
- vavr 的 Stream 是惰性链表,元素只有在必要的时候才会参与计算,因此大部分操作都可以在常量时间内完成。
函数(Functions)
Java 8 提供了接受一个参数的函数式接口 Function 和接受两个参数的函数式接口 BiFunction,vavr 则提供了最多可以接受 8 个参数的函数式接口:Function0、Function1、Function2、Function3、Function4……Function8。
vavr 还提供了更多函数式编程的特性:
- 组合(Composition)
在数学上,函数组合可以用两个函数形成第三个函数,例如函数 f:X->Y 和函数 g:Y->Z 可以组合成 h:g(f(x)),表示 X ->Z。这里看个组合的例子:
public class VavrFunctionExample {
@Test
public void testCompose() {
// 使用 andThen
Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
Function1<Integer, Integer> add1AndMultiplyBy2 = plusOne.andThen(multiplyByTwo);
Assert.assertEquals(6, add1AndMultiplyBy2.apply(2).intValue());
// 使用 compose
Function1<Integer, Integer> add1AndMultiplyBy2WithCompose = multiplyByTwo.compose(plusOne);
Assert.assertEquals(6, add1AndMultiplyBy2WithCompose.apply(2).intValue());
}
}
- Lifting
你是不是常常写这种代码:调用一个函数,判断它的返回值是否符合需求,或者需要 catch 所有异常以防异常情况,甚至是 catch(Throwable t)。Lifting 特性就是为了解决这个问题而存在的,可以在内部处理异常情况,并将异常转换成一个特殊的结果 None,这样函数外部就可以用统一的模式去处理函数结果。举个例子:
public class VavrFunctionExample {
@Test
public void testLifting() {Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);
// = None
Option<Integer> i1 = safeDivide.apply(1, 0);
Assert.assertEquals("None", i1.toString());
// = Some(2)
Option<Integer> i2 = safeDivide.apply(4, 2);
Assert.assertEquals(2, i2.get().intValue());
}
}
- 柯里化方法(Curring)
柯里化 (Currying) 指的是将原来接受多个参数的函数变成新的接受一个参数的函数的过程。对于 Java 来说,可以方便得提供默认值方法,这里看个例子:
public class VavrFunctionExample {
@Test
public void testCurried() {Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
Function1<Integer, Integer> add2 = sum.curried().apply(2);
Assert.assertEquals(6, add2.apply(4).intValue());
}
}
- 记忆化方法(Memorization)
这是一种缓存,某个方法只需要执行一次,后面都会返回第一次的结果;但是在实际应用中用到的地方应该不多。
public class VavrFunctionExample {
@Test
public void testMemorize() {
Function0<Double> hashCache =
Function0.of(Math::random).memoized();
double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();
Assert.assertTrue(randomValue1 == randomValue1);
}
}
模式匹配
模式匹配是函数式编程语言中的概念,目前 Java 中还不支持这个特性,使用 vavr 可以用 Java 写模式匹配的代码。Java 中的 switch…case 语句只能针对常量起作用,而使用模式匹配则可以对另一个函数的返回结果起作用,功能非常抢到。下面的例子分别给出了使用 if、switch…case、模式匹配三个语法实现同样功能的例子,可以看出,模式匹配有助于减少代码行数。
import org.junit.Test;
import static io.vavr.API.$;
import static io.vavr.API.Case;
import static io.vavr.API.Match;
import static org.junit.Assert.assertEquals;
public class VavrPatternExample {
@Test
public void whenIfWorksAsMatcher_thenCorrect() {
int input = 3;
String output;
if (input == 0) {output = "zero";}
if (input == 1) {output = "one";}
if (input == 2) {output = "two";}
if (input == 3) {output = "three";} else {output = "unknown";}
assertEquals("three", output);
}
@Test
public void whenSwitchWorksAsMatcher_thenCorrect() {
int input = 2;
String output;
switch (input) {
case 0:
output = "zero";
break;
case 1:
output = "one";
break;
case 2:
output = "two";
break;
case 3:
output = "three";
break;
default:
output = "unknown";
break;
}
assertEquals("two", output);
}
@Test
public void whenMatchworks_thenCorrect() {
int input = 2;
String output = Match(input).of(Case($(1), "one"),
Case($(2), "two"),
Case($(3), "three"),
Case($(), "?"));
assertEquals("two", output);
}
}
参考资料
- 《Java 8 实战》
- https://github.com/resilience4j/resilience4j
- https://www.baeldung.com/vavr
- https://www.vavr.io/vavr-docs/
本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。