1. 概述
本文将深入探讨 Vavr 库的核心价值、使用场景及实践方法。Vavr 是 Java 8+ 的函数式编程库,提供不可变数据类型和函数式控制结构。
1.1 Maven 依赖
使用 Vavr 前需添加依赖:
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.9.0</version>
</dependency>
建议始终使用最新版本,可通过 此链接 获取。
2. Option
Option 的核心目标是利用 Java 类型系统消除代码中的 null 检查。
Vavr 的 Option
类似 Java 8 的 Optional
,但实现了 Serializable
和 Iterable
,且 API 更丰富。在 Java 中,任何对象引用都可能为 null
,使用前需通过 if
语句检查:
@Test
public void givenValue_whenNullCheckNeeded_thenCorrect() {
Object possibleNullObj = null;
if (possibleNullObj == null) {
possibleNullObj = "someDefaultValue";
}
assertNotNull(possibleNullObj);
}
若无检查,应用可能因 NullPointerException
崩溃:
@Test(expected = NullPointerException.class)
public void givenValue_whenNullCheckNeeded_thenCorrect2() {
Object possibleNullObj = null;
assertEquals("somevalue", possibleNullObj.toString());
}
但检查会使代码 冗长且可读性差,尤其当 if
语句多层嵌套时。Option
通过完全消除 null
解决此问题:
null
值转为None
实例- 非
null
值转为Some
实例
@Test
public void givenValue_whenCreatesOption_thenCorrect() {
Option<Object> noneOption = Option.of(null);
Option<Object> someOption = Option.of("val");
assertEquals("None", noneOption.toString());
assertEquals("Some(val)", someOption.toString());
}
✅ 最佳实践:将对象包装在 Option
中,而非直接使用。Option
的 toString()
会返回有意义的值,无需 null
检查。
默认值处理
传统方式需显式检查:
@Test
public void givenNull_whenCreatesOption_thenCorrect() {
String name = null;
Option<String> nameOption = Option.of(name);
assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}
非 null
值同样适用:
@Test
public void givenNonNull_whenCreatesOption_thenCorrect() {
String name = "baeldung";
Option<String> nameOption = Option.of(name);
assertEquals("baeldung", nameOption.getOrElse("notbaeldung"));
}
⚠️ 优势:无需 null
检查,一行代码即可获取值或返回默认值。
3. 元组 (Tuple)
Java 没有原生元组结构。元组在函数式编程中很常见,不可变且能以类型安全的方式容纳多个不同类型的对象。
Vavr 为 Java 8 引入元组,支持 Tuple1
到 Tuple8
(最多 8 个元素)。通过 tuple._n
访问元素(n
从 1 开始):
public void whenCreatesTuple_thenCorrect1() {
Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
String element1 = java8._1;
int element2 = java8._2();
assertEquals("Java", element1);
assertEquals(8, element2);
}
❌ 注意:元组索引从 1 开始(非数组式 0 基)。元素类型需在声明时指定:
@Test
public void whenCreatesTuple_thenCorrect2() {
Tuple3<String, Integer, Double> java8 = Tuple.of("Java", 8, 1.8);
String element1 = java8._1;
int element2 = java8._2;
double element3 = java8._3;
assertEquals("Java", element1);
assertEquals(8, element2);
assertEquals(1.8, element3, 0.1);
}
✅ 典型场景:从方法返回多个对象时,元组比创建临时类更简洁。
4. Try
Vavr 中,**Try
是可能抛出异常的计算容器**。
正如 Option
包装可空对象避免 null
检查,Try
包装计算避免显式 try-catch
块。例如:
@Test(expected = ArithmeticException.class)
public void givenBadCode_whenThrowsException_thenCorrect() {
int i = 1 / 0;
}
无 try-catch
时应用崩溃。用 Vavr 包装后:
@Test
public void givenBadCode_whenTryHandles_thenCorrect() {
Try<Integer> result = Try.of(() -> 1 / 0);
assertTrue(result.isFailure());
}
可在代码任意位置检查计算状态。其他操作示例:
返回默认值:
@Test public void givenBadCode_whenTryHandles_thenCorrect2() { Try<Integer> computation = Try.of(() -> 1 / 0); int errorSentinel = computation.getOrElse(-1); assertEquals(-1, errorSentinel); }
抛出自定义异常:
@Test(expected = ArithmeticException.class) public void givenBadCode_whenTryHandles_thenCorrect3() { Try<Integer> result = Try.of(() -> 1 / 0); result.getOrElseThrow(ArithmeticException::new); }
⚠️ 核心价值:通过 Try
完全掌控计算后的行为。
5. 函数式接口
Java 8 内置函数式接口,但仅支持:
单参数函数(
Function
):@Test public void givenJava8Function_whenWorks_thenCorrect() { Function<Integer, Integer> square = (num) -> num * num; int result = square.apply(2); assertEquals(4, result); }
双参数函数(
BiFunction
):@Test public void givenJava8BiFunction_whenWorks_thenCorrect() { BiFunction<Integer, Integer, Integer> sum = (num1, num2) -> num1 + num2; int result = sum.apply(5, 7); assertEquals(12, result); }
Vavr 扩展至 最多 8 个参数,并新增记忆化、组合和柯里化等 API。接口按参数数命名:Function0
、Function1
等。示例:
@Test
public void givenVavrFunction_whenWorks_thenCorrect() {
Function1<Integer, Integer> square = (num) -> num * num;
int result = square.apply(2);
assertEquals(4, result);
}
@Test
public void givenVavrBiFunction_whenWorks_thenCorrect() {
Function2<Integer, Integer, Integer> sum =
(num1, num2) -> num1 + num2;
int result = sum.apply(5, 7);
assertEquals(12, result);
}
无参数时,Java 8 用 Supplier
,Vavr 用 Function0
:
@Test
public void whenCreatesFunction_thenCorrect0() {
Function0<String> getClazzName = () -> this.getClass().getName();
String clazzName = getClazzName.apply();
assertEquals("com.baeldung.vavr.VavrTest", clazzName);
}
五参数函数使用 Function5
:
@Test
public void whenCreatesFunction_thenCorrect5() {
Function5<String, String, String, String, String, String> concat =
(a, b, c, d, e) -> a + b + c + d + e;
String finalString = concat.apply(
"Hello ", "world", "! ", "Learn ", "Vavr");
assertEquals("Hello world! Learn Vavr", finalString);
}
✅ 技巧:通过 FunctionN.of
从方法引用创建函数。例如:
public int sum(int a, int b) {
return a + b;
}
@Test
public void whenCreatesFunctionFromMethodRef_thenCorrect() {
Function2<Integer, Integer, Integer> sum = Function2.of(this::sum);
int summed = sum.apply(5, 6);
assertEquals(11, summed);
}
6. 集合 (Collections)
Vavr 团队设计了 满足函数式编程要求的全新集合 API(持久化、不可变)。
Java 集合是可变的,易引发程序故障(尤其在并发场景)。Collection
接口包含此类方法:
interface Collection<E> {
void clear(); // 产生副作用,无返回值
}
为解决此类问题引入了 ConcurrentHashMap
,但 边际效益为零且降低性能。
不可变性天然保证线程安全,无需额外类库。Java 中现有不可变方案(如 Collections.unmodifiableList
)会抛出异常:
@Test(expected = UnsupportedOperationException.class)
public void whenImmutableCollectionThrows_thenCorrect() {
java.util.List<String> wordList = Arrays.asList("abracadabra");
java.util.List<String> list = Collections.unmodifiableList(wordList);
list.add("boom");
}
Vavr 集合完全规避上述问题。创建列表示例:
@Test
public void whenCreatesVavrList_thenCorrect() {
List<Integer> intList = List.of(1, 2, 3);
assertEquals(3, intList.length());
assertEquals(new Integer(1), intList.get(0));
assertEquals(new Integer(2), intList.get(1));
assertEquals(new Integer(3), intList.get(2));
}
支持直接计算:
@Test
public void whenSumsVavrList_thenCorrect() {
int sum = List.of(1, 2, 3).sum().intValue();
assertEquals(6, sum);
}
✅ 核心优势:
- 不可变性
- 消除
void
返回类型和副作用 API - 更丰富的元素操作函数
- 代码更简洁健壮
Vavr 集合覆盖 Java 集合框架大部分类,完整实现所有功能。本文不展开详述。
7. 验证 (Validation)
Vavr 将函数式编程中的 应用函子 (Applicative Functor) 概念引入 Java。简言之,应用函子允许在执行操作序列时累积结果。
vavr.control.Validation
类支持错误累积。通常程序遇错即终止,但 Validation
会继续处理并批量返回错误。
假设通过 name
和 age
注册用户,需先收集所有输入再决定创建 Person
或返回错误列表。Person
类如下:
public class Person {
private String name;
private int age;
// 标准构造器、getter/setter 和 toString
}
创建 PersonValidator
类,每个字段一个验证方法,组合结果到 Validation
实例:
class PersonValidator {
String NAME_ERR = "Invalid characters in name: ";
String AGE_ERR = "Age must be at least 0";
public Validation<Seq<String>, Person> validatePerson(
String name, int age) {
return Validation.combine(
validateName(name), validateAge(age)).ap(Person::new);
}
private Validation<String, String> validateName(String name) {
String invalidChars = name.replaceAll("[a-zA-Z ]", "");
return invalidChars.isEmpty() ?
Validation.valid(name)
: Validation.invalid(NAME_ERR + invalidChars);
}
private Validation<String, Integer> validateAge(int age) {
return age < 0 ? Validation.invalid(AGE_ERR)
: Validation.valid(age);
}
}
规则:age
≥ 0,name
无特殊字符:
@Test
public void whenValidationWorks_thenCorrect() {
PersonValidator personValidator = new PersonValidator();
Validation<List<String>, Person> valid =
personValidator.validatePerson("John Doe", 30);
Validation<List<String>, Person> invalid =
personValidator.validatePerson("John? Doe!4", -1);
assertEquals(
"Valid(Person [name=John Doe, age=30])",
valid.toString());
assertEquals(
"Invalid(List(Invalid characters in name: ?!4,
Age must be at least 0))",
invalid.toString());
}
✅ 关键点:
- 有效值在
Validation.Valid
实例中 - 错误列表在
Validation.Invalid
实例中 - 验证方法必须返回二者之一
8. 延迟 (Lazy)
Lazy
是 延迟计算值的容器:计算推迟至需要结果时执行,且结果会被缓存(记忆化),后续调用直接返回缓存值:
@Test
public void givenFunction_whenEvaluatesWithLazy_thenCorrect() {
Lazy<Double> lazy = Lazy.of(Math::random);
assertFalse(lazy.isEvaluated());
double val1 = lazy.get();
assertTrue(lazy.isEvaluated());
double val2 = lazy.get();
assertEquals(val1, val2, 0.1);
}
- 第 2 行:函数未执行(
isEvaluated()
返回false
) - 第 4 行:调用
get()
触发计算,isEvaluated()
变为true
- 第 7 行:再次调用
get()
返回缓存值(非重新计算)
⚠️ 记忆化验证:若函数重新执行,val1
和 val2
应不同(Math.random
),但实际相等,证明缓存生效。
9. 模式匹配 (Pattern Matching)
模式匹配是函数式编程语言的原生特性,Java 尚未支持。传统方案需多层 if
或 switch
:
@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);
}
或 switch
(平均 3 行/条件):
@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);
}
❌ 痛点:代码冗长,易漏 break
导致隐蔽错误。
Vavr 用 Match
替代整个 switch
块,Case
方法替代条件,原子模式 $()
替代条件表达式:
@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);
}
✅ 优势:平均 1 行/条件,代码紧凑。API 支持更复杂场景,如用谓词替换原子表达式(解析控制台命令):
Match(arg).of(
Case($(isIn("-h", "--help")), o -> run(this::displayHelp)),
Case($(isIn("-v", "--version")), o -> run(this::displayVersion)),
Case($(), o -> run(() -> {
throw new IllegalArgumentException(arg);
}))
);
谓词、多条件和副作用处理将在后续文章详解。
10. 总结
本文介绍了 Vavr——Java 8 流行的函数式编程库,覆盖了可快速提升代码质量的核心特性。完整示例代码见 GitHub 项目。