Vavr:颠覆你对 Java 的认知

Vavr:颠覆你对 Java 的认知

免责声明

本文内容仅代表作者个人经验,仅供参考。文中涉及的操作可能存在风险,请在尝试前务必备份重要数据。因参照本文操作导致的任何数据丢失、硬件损坏或其他损失,本人不承担任何法律责任。

Vavr Core 是一个轻量但体系完整的函数式编程库。它引入不可变集合、函数式控制结构以及多种语义化容器,帮助 Java 工程师在保持运行时兼容性的前提下享受到函数式世界的表达力。

Vavr 是什么

  • 不可变数据结构:提供 List、Map、Stream 等全套不可变集合,天然线程安全。
  • 函数式语法糖:函数式接口覆盖 0~8 个参数,额外提供柯里化、部分应用、组合等工具。
  • 容器模型OptionTryEitherValidation 等类型让错误处理和流程控制更加显式。
  • 语法增强:属性检查(Property Checking)、模式匹配等特性补齐原生 Java 的短板。

引入依赖

Maven

xml
<dependencies>
  <dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.10.4</version>
  </dependency>
</dependencies>

Gradle(Groovy DSL)

groovy
dependencies {
    compile "io.vavr:vavr:0.10.4"
}

Gradle 7+(推荐)

groovy
dependencies {
    implementation "io.vavr:vavr:0.10.4"
}

测试特性需要额外引入 io.vavr:vavr-test,详见属性检查章节。

1. Tuples(元组)

1.1 元组概念

在 Vavr 中,Tuple1 ~ Tuple8 表示固定长度、可承载不同类型的不可变组合。

  • 下标从 1 开始,通过 _1_2……访问。
  • 通常用于返回多值、承载中间状态,避免额外声明 DTO。

1.2 创建元组

java
final Tuple2<Integer, String> user = Tuple.of(1, "Eli auk");
Assert.equals(user._1, 1);
Assert.equals(user._2, "Eli auk");

使用 Tuple.of() 静态工厂创建,类型根据元素数量自动推断。

1.3 逐个映射元组组件

java
final Tuple2<Integer, String> mapped = user.map(
        a -> a + 1,
        b -> b + "1"
);
Assert.equals(mapped._1, 2);
Assert.equals(mapped._2, "Eli auk1");

1.4 使用单个映射函数

java
final Tuple2<Integer, String> mapped = user.map((a, b) -> Tuple.of(a + 1, b + "1"));
Assert.equals(mapped._1, 2);
Assert.equals(mapped._2, "Eli auk1");

当两个字段需要同时变换时可以直接返回新的 Tuple,避免多次迭代。

1.5 变换元组

java
final String result = user.apply((a, b) -> b.substring(2) + a);
Assert.equals(result, "i auk1");

apply() 直接把各元素当作函数参数,返回任意类型的结果,非常适合拼装字符串或计算派生值。

2. Function(函数)

2.1 函数式接口

Java 8 只内建了最多两个参数的函数式接口,Vavr 将其扩展为 Function0 ~ Function8,并提供 memoizedcurriedandThen 等方法,方便以 DSL 形式组织业务逻辑。

2.2 创建函数

匿名类

java
Function2<Integer, Integer, Integer> sum = new Function2<>() {
    @Override
    public Integer apply(Integer a, Integer b) {
        return a + b;
    }
};

Lambda

java
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;

静态工厂

java
final Function1<Integer, Integer> inc = Function1.of(a -> a + 1);

2.3 Composition(组合)

andThen:先当前再参数

java
Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiply = a -> a * 2;
Function1<Integer, Integer> pipeline = plusOne.andThen(multiply);
Assert.equals(pipeline.apply(2), 6);

compose:先参数再当前

java
Function1<Integer, Integer> pipeline = plusOne.compose(multiply);
Assert.equals(pipeline.apply(2), 5);

记忆口诀:andThen「先我后你」,compose「先你后我」。

2.4 Lifting(提升)

偏函数在非法入参时会抛异常。借助 lift() 可以包装为总函数:

java
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Option<Integer> safeDivide = Function2.lift(divide).apply(1, 0);
Assert.equals(safeDivide.isEmpty(), true);

2.5 Partial Application(部分应用)

将部分参数固定后得到新的函数,用于预置上下文。

java
Function2<Integer, Integer, Integer> sum = Integer::sum;
Function1<Integer, Integer> add1 = sum.apply(1);
Function5<Integer, Integer, Integer, Integer, Integer, Integer> sum5 = (a, b, c, d, e) -> a + b + c + d + e;
Function2<Integer, Integer, Integer> add6 = sum5.apply(2, 3, 1);
Assert.equals(add1.apply(2), 3);
Assert.equals(add6.apply(4).apply(5), 15);

柯里化会把 FunctionN 拆成一串单参数函数:

java
Function1<Integer, Integer> add1Curried = sum.curried().apply(1);
Function1<Integer, Function1<Integer, Function1<Integer, Function1<Integer, Integer>>>> curried = sum5.curried().apply(1);
Assert.equals(add1Curried.apply(2), 3);
Assert.equals(curried.apply(2).apply(3).apply(4).apply(5), 15);

差异总结:

  1. 语法:部分应用使用 .apply() 指定多个参数,柯里化先 .curried() 再逐个 .apply()
  2. 返回类型:部分应用直接得到新的 FunctionK,柯里化永远返回单参数函数链。
  3. 灵活性:部分应用可以一次绑定多个参数,柯里化更适合链式组合。

2.6 Memoization(记忆化)

记忆化函数只计算一次,再次调用直接读取缓存;相比直接存变量,函数懒执行且可组合。

java
Function0<Integer> randomGenerator = Function0.of(() -> new Random().nextInt()).memoized();
Integer first = randomGenerator.apply();
Integer second = randomGenerator.apply();
Assert.equals(first, second);

常见场景:缓存昂贵计算、懒加载资源、组合多段逻辑或在单元测试中模拟依赖。

3. Value(值)

3.1 Option

封装可能为空的返回值,API 风格与 Stream-QueryOpp 类似,可链式 mapgetOrElse,避免显式空指针判断。

3.2 Try

捕获异常并将其转化为 Success / Failure,也可参考 Stream-QueryOpp.ofTry 的写法,通过 recoveronFailure 等方法快速处理错误。

3.3 Lazy

惰性求值的容器,自带记忆化能力。

java
Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // false
lazy.get();         // 0.123...
lazy.isEvaluated(); // true
Assert.equals(lazy.get(), lazy.get());

Supplier 相比,Lazy 只执行一次,而 Supplier 每次都会重新计算。

3.4 Either

左值通常代表错误,右值代表成功。

java
public Either<String, Integer> compute() {
    if (RandomUtil.randomBoolean()) {
        return Either.right(42);
    }
    return Either.left("Computation failed");
}

Either<String, Integer> value = compute().map(i -> i * 2);
if (value.isRight()) {
    System.out.println("Success: " + value.get());
} else {
    System.out.println("Failure: " + value.getLeft());
}

3.5 Validation

Validation 是一个应用函子,适合汇总多个字段的错误。

4. Collection(集合)

Vavr 的集合默认不可变,配合链式 API 带来更少的样板代码。

4.1 List

java
Optional<Integer> reduce = Stream.of(1, 2, 3).reduce(Integer::sum);
int sum = IntStream.of(1, 2, 3).sum();
Number listSum = List.of(1, 2, 3).sum();
Assert.equals(listSum.intValue(), sum);

4.2 Stream

因为内建了元组,Stream 的转换能力比 JDK 版本更友好。

java
java.util.Map<Integer, Character> vanilla = Stream.of(1, 2, 3)
        .collect(Collectors.toMap(a -> a, b -> (char) (b + 64)));
Map<Integer, Character> vavrMap = List.of(1, 2, 3).toMap(a -> a, b -> (char) (b + 64));
Assert.equals(vavrMap.get(1).get(), vanilla.get(1));

HashMap<Integer, Character> javaMap = List.of(1, 2, 3)
        .toJavaMap(HashMap::new, a -> Tuple.of(a, (char) (a + 64)));
Assert.equals(javaMap.get(1), 'A');

5. Property Checking(属性检查)

需要额外依赖:

xml
<dependency>
  <groupId>io.vavr</groupId>
  <artifactId>vavr-test</artifactId>
  <version>0.10.4</version>
</dependency>

示例:

java
Arbitrary<Integer> ints = Arbitrary.integer();

Property.def("square(int) >= 0")
        .forAll(ints)
        .suchThat(i -> i * i >= 0)
        .check()
        .assertIsSatisfied();

6. Pattern Matching(模式匹配)

6.1 基础匹配

java
String s = Match(1).of(
        Case($(1), "one"),
        Case($(2), "two"),
        Case($(), "?")
);
Assert.equals(s, "one");

String opt = Match(null).of(
        Case($(1), "one"),
        Case($(2), "two"),
        Case($(), "?")
);
Assert.equals(opt, "?");

6.2 多条件匹配

java
int input = 5;
String output = Match(input).of(
        Case($(n -> n > 0 && n < 3), "Between 1 and 2"),
        Case($(n -> n > 3 && n < 6), "Between 4 and 5"),
        Case($(), "Other")
);
Assert.equals(output, "Between 4 and 5");

6.3 断言匹配

java
String s = Match(2).of(
        Case($(a -> a == 1), "one"),
        Case($(a -> a == 2), "two"),
        Case($(), "?")
);
Assert.equals(s, "two");

内置断言(is()isIn()isNotNull()isZero() 等)可以让条件更语义化:

java
String opt = Match(null).of(
        Case($(is(1)), "one"),
        Case($(is(2)), "two"),
        Case($(), "?")
);
Assert.equals(opt, "?");

通过 Vavr,可以在保持 Java 生态兼容的同时拥抱不可变模型与函数式表达式,让业务代码更简洁、更安全。

Arthas的简单使用
组件样式示例

评论区

评论加载中...