首页 > 编程语言 >Vavr - java函数式编程,分离业务代码与非业务代码神器

Vavr - java函数式编程,分离业务代码与非业务代码神器

时间:2024-10-10 22:49:13浏览次数:8  
标签:Case java 函数 代码 Vavr linear Java Option

微信公众号:阿俊的学习记录空间

小红书:ArnoZhang

wordpress:arnozhang1994

博客园:arnozhang

CSDN:ArnoZhang1994

1. 入门指南

使用 Vavr 的项目至少需要支持 Java 1.8。

该 .jar 文件可以在 Maven Central 获取。

1.1. Gradle

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

Gradle 7+ 版本:

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

1.2. Maven

<!--  -->
<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>1.0.0-alpha-4</version>
</dependency>

1.3. 独立使用

由于 Vavr 不依赖任何其他库(除了 JVM),你可以很容易地将其作为独立的 .jar 文件添加到类路径中。

2. 使用指南

Vavr 提供了一些设计良好的基本类型表示,这些类型在 Java 中显然缺失或只是初步支持:Tuple、Value 和 λ。
在 Vavr 中,一切都基于这三个基本构建模块:

Vavr 概述

2.1. 元组 (Tuples)

Java 缺少通用的元组概念。元组将固定数量的元素组合在一起,使它们可以作为整体传递。与数组或列表不同,元组可以包含不同类型的对象,并且它们是不可变的。
元组类型为 Tuple1、Tuple2、Tuple3 等,目前元素个数上限为 8。要访问元组 t 的元素,可以使用 t._1 访问第一个元素,t._2 访问第二个元素,依此类推。

2.1.1. 创建元组

下面是如何创建一个包含 String 和 Integer 的元组的示例:

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

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

// 8
Integer i = java8._2;
  • 使用静态工厂方法 Tuple.of() 创建元组。
  • 获取该元组的第一个元素。
  • 获取该元组的第二个元素。

2.1.2. 分别映射元组的组件

通过分别映射元组的组件,可以对元组中的每个元素应用一个函数,返回另一个元组。

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

2.1.3. 使用单个映射器映射元组

也可以使用一个映射函数映射元组。

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

2.1.4. 转换元组

转换可以基于元组的内容创建新类型。

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

2.2. 函数 (Functions)

函数式编程的核心是值以及通过函数对值进行的转换。Java 8 提供了一个 Function 接口,接受一个参数,还有 BiFunction,接受两个参数。而 Vavr 提供了最多可接受 8 个参数的函数。函数式接口类型为 Function0、Function1、Function2、Function3 等。如果需要抛出受检异常的函数,可以使用 CheckedFunction1、CheckedFunction2 等。

下面的 lambda 表达式创建了一个求和两个整数的函数:

// 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;
    }
};

你也可以使用静态工厂方法 Function3.of(…) 从任何方法引用创建函数。

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

事实上,Vavr 的函数式接口是 Java 8 函数式接口的增强版。它们还提供以下功能:

  • 组合 (Composition)
  • 提升 (Lifting)
  • Currying (Currying)
  • Memoization (Memoization)

2.2.1. 组合 (Composition)

你可以组合函数。在数学中,函数组合是将一个函数的结果作为另一个函数的输入来生成第三个函数。例如,函数 f: X → Y 和 g: Y → Z 可以组合为 h: g(f(x)),它将 X 映射到 Z。
你可以使用 andThen

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);

或使用 compose

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

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

2.2.2. 提升 (Lifting)

你可以将部分函数提升为一个返回 Option 结果的总函数。部分函数的概念来自数学。部分函数 f: X′ → Y 是从 X 的某个子集 X′ 到 Y 的函数,它没有要求函数 f 将 X 中的每个元素映射到 Y 中的元素。部分函数仅对某些输入值有效。

例如,下面的方法 divide 是一个仅接受非零除数的部分函数。

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

我们使用 liftdivide 转换为对所有输入定义的总函数。

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

例如,下面的方法 sum 仅接受正数作为输入。

int sum(int first, int second) {
    if (first < 0 || second < 0) {
        throw new IllegalArgumentException("Only positive integers are allowed"); 
    }
    return first + second;
}
  • 如果输入为负数,sum 函数会抛出 IllegalArgumentException

我们可以通过方法引用提升 sum 方法。

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

// = None
Option<Integer> optionalResult = sum.apply(-1, 2); 
  • 提升的函数捕获 IllegalArgumentException 并将其映射为 None

2.2.3. 部分应用 (Partial application)

部分应用允许你通过固定某些参数从现有函数派生一个新函数。你可以固定一个或多个参数,固定的参数数量决定了新函数的元数,即 新元数 = 原元数 - 固定参数。参数按从左到右的顺序绑定。

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

then(add2.apply(4)).isEqualTo(6);
  • 第一个参数 a 被固定为值 2。

通过固定 Function5 的前三个参数,可以得到一个 Function2

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);
  • 参数 abc 分别被固定为值 2、3 和 1。

2.2.4. Currying

Currying是一种部分应用函数的技术,通过为某个参数固定一个值,产生一个返回 Function1Function1 函数。

当对 Function2 进行Currying时,结果与 Function2 的部分应用没有区别,因为两者都返回一个只带一个参数的函数。

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

第一个参数 a 被固定为值 2。

你可能会注意到,除了 .curried() 的使用之外,此代码与部分应用中的 2 参数例子完全相同。随着参数数量的增加,差异变得明显。

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,直到最终的调用为止。

2.2.5. Memoization

Memoization是一种缓存形式。一个Memoization函数只会执行一次,然后从缓存中返回结果。以下示例首次调用时计算一个随机数,第二次调用时返回缓存的数字。

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

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

then(randomValue1).isEqualTo(randomValue2);

2.3. 值

在函数式编程中,我们将值视为一种标准形式,即不能进一步计算的表达式。在 Java 中,我们通过将对象的状态设为 final 来表示这一点,并称之为不可变对象。Vavr 的函数式 Value 抽象了不可变对象。通过在实例之间共享不可变内存,添加了高效的写操作,我们免费获得了线程安全!

2.3.1. Option

Option 是一种单子容器类型,用于表示一个可选值。Option 的实例要么是 Some 的实例,要么是 None

// 可选 *value*,没有更多的 nulls
Option<T> option = Option.of(...);

如果你使用过 Java 的 Optional 类,注意两者之间的一个关键区别。在 Optional 中,调用 .map 返回 null 会导致一个空的 Optional,而在 Vavr 中,它会返回 Some(null),这可能会导致 NullPointerException

使用 Optional 时,以下场景是有效的:

Optional<String> maybeFoo = Optional.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
Optional<String> maybeFooBar = maybeFoo.map(s -> (String)null)
                                       .map(s -> s.toUpperCase() + "bar");
then(maybeFooBar.isPresent()).isFalse();

选项是 Some("foo"),然后选项变为空。

使用 Vavr 的 Option,相同的场景会导致 NullPointerException

Option<String> maybeFoo = Option.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
try {
    maybeFoo.map(s -> (String)null)
            .map(s -> s.toUpperCase() + "bar");
    Assert.fail();
} catch (NullPointerException e) {
    // 显然这不是正确的方法
}

选项是 Some("foo"),结果变为 Some(null),对 s.toUpperCase() 的调用将在 null 上进行。

虽然这看起来像是 Vavr 的实现有问题,但实际上它遵循了单子的要求,即在调用 .map 时保留计算上下文。对于 Option 来说,这意味着对 Some 调用 .map 会返回 Some,对 None 调用 .map 会返回 None。在上面的 Java Optional 示例中,上下文从 Some 变成了 None

这似乎使 Option 变得无用,但它实际上强迫你关注可能出现的 null,并相应处理。处理 null 的正确方法是使用 flatMap

Option<String> maybeFoo = Option.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
Option<String> maybeFooBar = maybeFoo.map(s -> (String)null)
                                     .flatMap(s -> Option.of(s)
                                                         .map(t -> t.toUpperCase() + "bar"));
then(maybeFooBar.isEmpty()).isTrue();

选项是 Some("foo"),结果选项是 Some(null)snull,因此变为 None

或者,你可以将 .flatMap 与可能为 null 的值一起使用:

Option<String> maybeFoo = Option.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
Option<String> maybeFooBar = maybeFoo.flatMap(s -> Option.of((String)null))
                                     .map(s -> s.toUpperCase() + "bar");
then(maybeFooBar.isEmpty()).isTrue();

选项是 Some("foo"),结果选项是 None

这一点在 Vavr 博客上有更详细的探讨。

2.3.2. Try

Try 是一种单子容器类型,用于表示可能抛出异常的计算,或返回成功的计算结果。它与 Either 类似,但语义上不同。Try 的实例要么是 Success,要么是 Failure

// 不需要处理异常
Try.of(() -> bunchOfWork()).getOrElse(other);
import static io.vavr.API.*;        // $, Case, Match
import static io.vavr.Predicates.*; // instanceOf

A result = Try.of(this::bunchOfWork)
    .recover(x -> Match(x).of(
        Case($(instanceOf(Exception_1.class)), t -> somethingWithException(t)),
        Case($(instanceOf(Exception_2.class)), t -> somethingWithException(t)),
        Case($(instanceOf(Exception_n.class)), t -> somethingWithException(t))
    ))
    .getOrElse(other);

2.3.3. Lazy

Lazy 是一种单子容器类型,表示延迟计算的值。与 Supplier 相比,Lazy 是Memoization的,即它只计算一次,因此具有引用透明性。

Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // = false
lazy.get();         // = 0.123(随机生成)
lazy.isEvaluated(); // = true
lazy.get();         // = 0.123(已记忆)

你还可以创建一个真正的惰性值(仅适用于接口):

CharSequence chars = Lazy.val(() -> "Yay!", CharSequence.class);

2.3.4. Either

Either 表示两种可能类型的值。Either 要么是 Left,要么是 Right。如果 EitherRight,并投影为 Left,则 Left 的操作不会影响 Right 的值。反之亦然。如果 Left 投影为 LeftRight 投影为 Right,则操作会生效。

例如:compute() 函数可能会返回一个整数值(成功的情况下)或一个类型为 String 的错误消息(失败的情况下)。按照惯例,成功的情况为 Right,失败的情况为 Left

Either<String, Integer> value = compute().right().map(i -> i * 2).toEither();

如果 compute() 的结果是 Right(1),则 valueRight(2)
如果 compute() 的结果是 Left("error"),则 valueLeft("error")

2.3.5. Future

Future 表示一个最终可用的计算结果。提供的所有操作都是非阻塞的。底层的 ExecutorService 被用于执行异步处理器,例如通过 onComplete(...)

Future 有两种状态:待定和已完成。

  • 待定:计算正在进行中。只有待定的 Future 可以完成或取消。
  • 已完成:计算成功完成并有结果,或因异常失败,或已取消。

回调可以在任意时间点注册到 Future 上。这些操作将在 Future 完成后执行。如果操作注册到已完成的 Future,则会立即执行。操作可能在一个单独的线程上运行,具体取决于

提供的 ExecutorService

3.3.6. Validation

Validation 控制是一种应用函子,能够累积错误。当我们尝试组合 Monads 时,通常组合过程会在遇到第一个错误时短路,而 Validation 则会继续处理组合函数,累积所有的错误。这在处理多个字段的校验时尤其有用,比如一个网页表单,你希望一次性得到所有的错误,而不是一个个逐步处理。

例如:我们从一个网页表单中获取字段 nameage,希望生成一个有效的 Person 实例,或者返回校验错误的列表。

PersonValidator personValidator = new PersonValidator();

// Valid(Person(John Doe, 30))
Validation<Seq<String>, Person> valid = personValidator.validatePerson("John Doe", 30);

// Invalid(List(Name contains invalid characters: '!4?', Age must be greater than 0))
Validation<Seq<String>, Person> invalid = personValidator.validatePerson("John? Doe!4", -1);

一个有效的值包含在 Validation.Valid 实例中,一个校验错误列表包含在 Validation.Invalid 实例中。

以下校验器用于将不同的校验结果组合为一个 Validation 实例。

class PersonValidator {

    private static final String VALID_NAME_CHARS = "[a-zA-Z ]";
    private static final int MIN_AGE = 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) {
        return CharSeq.of(name).replaceAll(VALID_NAME_CHARS, "").transform(seq -> seq.isEmpty()
                ? Validation.valid(name)
                : Validation.invalid("Name contains invalid characters: '"
                + seq.distinct().sorted() + "'"));
    }

    private Validation<String, Integer> validateAge(int age) {
        return age < MIN_AGE
                ? Validation.invalid("Age must be at least " + MIN_AGE)
                : Validation.valid(age);
    }

}

如果校验成功,即输入数据有效,则会根据给定的 nameage 创建一个 Person 实例。

class Person {

    public final String name;
    public final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person(" + name + ", " + age + ")";
    }

}

3.4. Collections

在设计一个全新的 Java 集合库时,Vavr 付出了大量努力,确保其满足函数式编程的要求,特别是不可变性。

Java 的 Stream 提升了计算层次,需通过显式步骤将计算与特定集合链接起来。而使用 Vavr,我们不需要这些额外的样板代码。

Vavr 的新集合基于 java.lang.Iterable,因此可以利用更加简洁的迭代方式。

// 1000 个随机数
for (double random : Stream.continually(Math::random).take(1000)) {
    ...
}

TraversableOnce 提供了大量有用的函数用于操作集合,其 API 类似于 java.util.stream.Stream,但更加成熟。

3.4.1. List

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();

3.4.2. Stream

io.vavr.collection.Stream 是懒加载的链表。值仅在需要时计算。由于其懒加载特性,大多数操作可以在常数时间内完成。操作通常是中间性的,并在一次遍历中执行。

令人惊叹的是,流可以用于表示理论上无限长的序列。

// 2, 4, 6, ...
Stream.from(1).filter(i -> i % 2 == 0);

3.4.3. Performance Characteristics

Table 1. Time Complexity of Sequential Operations

Type head() tail() get(int) update(int, T) prepend(T) append(T)
Array const linear const const linear linear
CharSeq const linear const linear linear linear
Iterator const const
List const const linear linear const linear
Queue const consta linear linear const const
PriorityQueue log log log log
Stream const const linear linear constlazy constlazy
Vector consteff consteff const eff const eff const eff const eff

Table 2. Time Complexity of Map/Set Operations

Type contains/Key add/put remove min
HashMap consteff consteff consteff linear
HashSet consteff consteff consteff linear
LinkedHashMap consteff linear linear linear
LinkedHashSet consteff linear linear linear
Tree log log log log
TreeMap log log log log
TreeSet log log log log

说明:

  • const — 常数时间
  • consta — 平摊常数时间,少数操作可能会耗时更长
  • consteff — 有效常数时间,取决于诸如哈希键分布等假设
  • constlazy — 延迟常数时间,操作会被推迟执行
  • log — 对数时间
  • linear — 线性时间

3.5. 属性检测

属性检测(也称为属性测试)是一种非常强大的测试代码属性的方式,基于生成的随机数据,这些数据会传递给用户定义的检查函数。

Vavr 在其 io.vavr:vavr-test 模块中提供了属性测试支持,因此确保在测试中引入此模块。

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

// square(int) >= 0: OK, 通过了 1000 次测试。
Property.def("square(int) >= 0")
        .forAll(ints)
        .suchThat(i -> i * i >= 0)
        .check()
        .assertIsSatisfied();

复杂数据结构的生成器是由简单生成器组成的。


3.6. 模式匹配

Scala 拥有原生的模式匹配,这是相较于纯 Java 的优势之一。基本语法与 Java 的 switch 相似:

val s = i match {
  case 1 => "one"
  case 2 => "two"
  case _ => "?"
}

值得注意的是,match 是一个表达式,它会产生一个结果。此外,它还提供了:

  • 命名参数:case i: Int ⇒ "Int " + i
  • 对象解构:case Some(i) ⇒ i
  • 守卫:case Some(i) if i > 0 ⇒ "positive " + i
  • 多条件匹配:case "-h" | "--help" ⇒ displayHelp
  • 编译时对穷举性的检查

模式匹配是一项非常有用的功能,它可以避免我们编写大量的 if-then-else 分支。它减少了代码量,并让我们专注于相关部分。

3.6.1. Java 的 Match 基础

Vavr 提供了一个类似于 Scala 的 match API。通过添加以下 import 来启用:

import static io.vavr.API.*;

在范围内包含静态方法 MatchCase 以及以下原子模式:

  • $() - 通配符模式
  • $(value) - 相等模式
  • $(predicate) - 条件模式

我们可以将初始的 Scala 示例表达为:

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

⚡ 我们使用统一的全大写方法名,因为 case 是 Java 的关键字,这使得 API 显得特别。

穷举性

最后的通配符模式 $() 可以避免抛出 MatchError,如果没有匹配到任何情况时会抛出该错误。

由于我们无法像 Scala 编译器那样进行穷举性检查,我们可以选择返回一个可选结果:

Option<String> s = Match(i).option(
    Case($(0), "zero")
);

语法糖

如前所示,Case 允许匹配条件模式。

Case($(predicate), ...)

Vavr 提供了一组默认谓词:

import static io.vavr.Predicates.*;

这些可以用于表达初始的 Scala 示例:

String s = Match(i).of(
    Case($(is(1)), "one"),
    Case($(is(2)), "two"),
    Case($(), "?")
);

多条件

我们使用 isIn 谓词来检查多个条件:

Case($(isIn("-h", "--help")), ...)

执行副作用

Match 作为一个表达式,它会产生一个值。为了执行副作用,我们需要使用辅助函数 run,它返回 Void

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);
    }))
);

run 用来避免歧义,因为 void 不是 Java 中有效的返回值。

注意:run 不应作为直接返回值使用,即在 lambda 体外使用:

// 错误用法!
Case($(isIn("-h", "--help")), run(this::displayHelp))

否则,在匹配模式之前,Case 会被急切地执行,从而破坏整个 Match 表达式。我们应在 lambda 体内使用它:

// 正确用法
Case($(isIn("-h", "--help")), o -> run(this::displayHelp))

可以看到,如果使用不当,run 会导致错误。请小心使用。我们考虑在未来的版本中弃用它,并可能为副作用操作提供更好的 API。

命名参数

Vavr 使用 lambda 提供了匹配值的命名参数:

Number plusOne = Match(obj).of(
    Case($(instanceOf(Integer.class)), i -> i + 1),
    Case($(instanceOf(Double.class)), d -> d + 1),
    Case($(), o -> { throw new NumberFormatException(); })
);

到目前为止,我们直接使用原子模式进行值匹配。如果原子模式匹配成功,匹配对象的正确类型会从上下文中推断出来。

接下来,我们将看一下递归模式,它可以匹配(理论上)任意深度的对象图。

对象解构

在 Java 中,我们使用构造函数实例化类。我们理解对象解构为将对象拆解成其部分。

构造函数是一个函数,它接受参数并返回一个新实例,而解构函数则接受一个实例并返回其部分。我们称对象为未应用的。

对象解构不一定是唯一的操作。例如,一个 LocalDate 可以被解构为:

  • 年、月和日组成的组件
  • 表示相应 Instant 的纪元毫秒数的长整型值
  • 等等

3.6.2. 模式

在 Vavr 中,我们使用模式来定义如何解构特定类型的实例。这些模式可以与 Match API 结合使用。

预定义模式

对于许多 Vavr 类型,已经存在匹配模式。它们通过以下方式引入:

import static io.vavr.Patterns.*;

例如,现在我们可以匹配 Try 的结果:

Match(_try).of(
    Case($Success($()), value -> ...),
    Case($Failure($()), x -> ...)
);

⚡ Vavr 的 Match API 的第一个原型允许从匹配模式中提取用户定义的对象选择。但由于缺乏适当的编译器支持,这种做法并不可行,因为生成的方法数量会呈指数增长。当前的 API 作出了妥协:所有模式都会匹配,但只有根模式会被解构。

Match(_try).of(
    Case($Success($Tuple2($("a"), $())), tuple2 -> ...),
    Case($Failure($(instanceOf(Error.class))), error -> ...)
);

这里的根模式是 SuccessFailure。它们分别被解构为 Tuple2Error,并具有正确的泛型类型。

⚡ 深度嵌套类型的推断是根据 Match 参数进行的,而不是根据匹配的模式。

用户定义的模式

能够解构任意对象,包括 final 类的实例,是至关重要的。Vavr 通过提供编译时注解 @Patterns@Unapply 以声明式的方式实现这一功能。

要启用注解处理器,需要将 vavr-match 作为项目依赖添加。

⚡ 注意:当然,也可以不使用代码生成器,直接实现这些模式。有关更多信息,请查看生成的源代码。

import io.vavr.match.annotation.*;

@Patterns
class My {

    @Unapply
    static <T> Tuple1<T> Optional(java.util.Optional<T> optional) {
        return Tuple.of(optional.orElse(null));
    }
}

注解处理器会在同一包中生成一个名为 MyPatterns 的文件(默认位于 target/generated-sources 中)。内部类也支持这种处理。特殊情况:如果类名为 $,生成的类名就只是 Patterns,没有前缀。

守卫

现在我们可以使用守卫匹配 Optional

Match(optional).of(
    Case($Optional($(v -> v != null)), "defined"),
    Case($Optional($(v -> v == null)), "empty")
);

这些谓词可以通过实现 isNullisNotNull 来简化。

⚡ 是的,提取 null 的确很奇怪。与其使用 Java 的 Optional,不如尝试 Vavr 的 Option

Match(option).of(
    Case($Some($()), "defined"),
    Case($None(), "empty")
);

标签:Case,java,函数,代码,Vavr,linear,Java,Option
From: https://www.cnblogs.com/arnozhang/p/18457367

相关文章

  • 毕业设计项目-基于JavaWeb技术的在线考试系统设计与实现源码+万字论文
    项目简介基于springboot实现的,主要功能如下:技术栈后端框框:springboot/mybatis前端框架:html/JavaScript/Css/vue/elementui运行环境:JDK1.8/MySQL5.7/idea(可选)/Maven3(可选)/tomcat8+(可选)jdk版本:最好是javajdk1.8,我们在这个平台上运行的,其他版本理论上也可以是否需要mave......
  • 代码随想录算法训练营day11|150. 逆波兰表达式求值 239. 滑动窗口最大值 347.前 K
    学习资料:https://programmercarl.com/0150.逆波兰表达式求值.html#算法公开课栈、队列、堆学习记录:150.逆波兰表达式求值(中序表达式转换为后序表达式,用栈实现;遇到符号就从栈中取前两个元素进行运算,再放回去)点击查看代码fromoperatorimportadd,sub,muldefdiv(x,y):......
  • eclipse导入文件java环境不适配时怎么办
    比如你在Java8.0环境下写的代码,但是导入另一台Java环境为17的电脑,将会出现如下状况:所以我们需要进行改动首先右键单击导入的包,选择properties然后进行如下操作最后红色的感叹号就成功消失啦~......
  • CAS存在的问题及在Java中的解决方式
    CAS介绍CAS可以保证对共享变量操作的原子性CAS全称CompareAndSwap,比较与交换,是乐观锁的主要实现方式。CAS在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock内部的AQS和原子类内部都使用了CAS。CAS算法涉及到三个操作数:需要读写的内存值V。进行比较的值A。......
  • 基于JAVA+SpringBoot+Vue+协同过滤算法+爬虫的前后端分离的租房系统
    ✌全网粉丝20W+,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌......
  • Java 初学 day10
    Java10常用类1、API概述API(ApplicationProgrammingInterface)应用程序编程接口编写应该机器人程序去控制机器人踢足球,程序就需要向机器人发出向前跑、向后跑、射门、抢球等各种命令,没有编过程序的人很难想象这样的程序如何编写。但是对于有经验的开发人员来说,知道机器人......
  • Java如何写一个构造函数
     构造函数是类的一个特殊成员函数,它在创建对象时被调用,用于初始化新创建的对象。在Java中,构造函数的名称必须与类名完全相同,没有返回类型(包括void)。构造函数可以有参数,也可以没有。Java中的构造函数示例假设我们想要创建一个Person类,包含name和age两个属性。我们可以这样定......
  • java级开发面试八股文
    1、java基础知识Q1、equals和==的区别==是判断两个变量或实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同。==是对内存地址进行比较,而equals比较的是两个字符串的值是否相等。==指引用是否相同,而equals是比较值是否相同。Q2:集合的父......
  • java+vue计算机毕设高校科研信息管理系统【源码+程序+论文+开题】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着高等教育的快速发展和科研活动的日益复杂化,高校科研管理面临着前所未有的挑战。传统的科研管理方式已难以满足当前高效、透明、协同的管理需求。......
  • java+vue计算机毕设高校普法系统【源码+程序+论文+开题】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在当今社会,法治观念深入人心,法律素养已成为衡量公民综合素质的重要指标之一。高校作为培养社会精英的摇篮,其普法教育的成效直接影响到未来社会的法治......