当前位置: 首页 > 工具软件 > Vavr > 使用案例 >

Java8+函数式开发库Vavr使用入门

汤兴生
2023-12-01

Java 8+ 函数式库Vavr功能简介

1 概述

Vavr 是Java 8+中一个函数式库,提供了一些不可变数据类型及函数式控制结构。

1.1 Maven 依赖

添加依赖,可以到maven仓库中查看最新版本。

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.10.3</version>
</dependency>

2. Option

Option的作用是消除代码中的null检查。在Vavr中Option是一个对象容器,与Optional类似,有一个最终结果。 Vavr中的Option实现了Serializable, Iterable接口,并且具有更加丰富的API。在Java中,我们通常通过if语句来检查引用是否为null,以此来保证系统健壮与稳定。如果不检查会出现空指针异常。

@Test
public void givenValue_whenNullCheckNeeded_thenCorrect() {
    Object object = null;
    if (object == null) {
        object = "someDefaultValue";
    }
    assertNotNull(possibleNullObj);
}

如果包含较多的if检查,同时带有嵌套语句,那么代码开始变得臃肿。Option通过将null替换为一个有效对象来解决这个问题。使用Option null值会通过None实例来表示,而非null值则是某个具体对象实例。

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

代码中调用toString时,并没有进行检查来处理NullPointerException问题。Option的toString会返回给我们一个有意义的值,这里是 “None”。当值为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中没有与元组(Tuple)相对应的结构。Tuple是函数式编程中一种常见的概念。Tuple是一个不可变,并且能够以类型安全的形式保存多个不同类型的对象。Tuple中最多只能有8个元素。

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。

Tuple中的元素必须是所声明的类型。

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

当需要返回多个对象时可以考虑使用Tuple。

4. Try

在Vavr, Try是一个容器,来包装一段可能产生异常的代码。Option用来包装可能产生null的对象,而Try用来包装可能产生异常的代码块,这样就不用显式的通过try-catch来处理异常。下面的代码用来检查是否产生了异常。

@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 = result.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);
}

5. 函数式接口

Java 8中的函数式接口最多接收两个参数,Vavr对其进行了扩展,最多支持8个参数。

@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使用方法引用来创建一个Vavr函数。

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

Java中的集合通常是可变集合,这通常是造成错误的根源。特别是在并发场景下。
此外Jdk中的集合类存在一些不足。例如JDK中的集合接口提供的一个方法clear,
该方法删除所有元素而且没有返回值。

interface Collection<E> {
    void clear();
}

在并发场景下大多集合都会会产生问题,因此有了诸如ConcurrentHashMap这样的类。
此外JDK还通过一些其它的方法创建不可变集集合,但误用某些方法时会产生异常。如
下,创建不可修改List,在误调用add的情况下会产生UnsupportedOperationException
异常。

@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中的集合则会避免这些问题,并且保证了线程安全、不可变等特性。在Vavr中创建一个list,实例并且不包含那些会导致UnsupportedOperationException异常的方法,且不可变,这样避免误用,造成错误。

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

此外还可以通过提供的API执行计算任务。

@Test
public void whenSumsVavrList_thenCorrect() {
    int sum = List.of(1, 2, 3).sum().intValue();

    assertEquals(6, sum);
}

Vavr集合提供了在Java集合框架中绝大多数常见的类,并且实现了其所有特征。Vavr提供的集合工具使得编写的代码更加紧凑,健壮,并且提供了丰富的功能。

7. 验证Validation

Vavr将函数式编程中 Applicative Functor(函子)的概念引入Java。vavr.control.Validation类能够将错误整合。通常情况下,程序遇到错误就,并且未做处理就会终止。然而,Validation会继续处理,并将程序错误累积,最终最为一个整体处理。
例如我们希望注册用户,用户具有用户名和密码。我们会接收一个输入,然后

决定是否创建Person实例或返回一个错误。Person类如下。

public class Person {
    private String name;
    private int age;

    // setters and getters, toString
}

接着,创建一个PersonValidator类。每个变量都会有一个方法来验证。此外还有方法可以将所有的验证结果整合到一个Validation实例中。

class PersonValidator {
    String NAME_ERR = "Invalid characters in name: ";
    String AGE_ERR = "Age must be at least 0";

    public Validation<List<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包含了错误。因此validation要么
返回有效值要么返回无效值。Validation.Valid内部是一个Person实例,而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);
}

上面的例子中,我们执行的计算是Math.random。当我们调用isEvaluated检查状态时,发现函数并没有被执行。随后调用get方法,我们得到计算的结果。第2次调用get时,再次返回之前计算的结果,而之前的计算结果已被缓存。

9. 模式匹配Pattern Matching

当我们执行一个计算或根据输入返回一个满足条件的值时,我们通常会用到if语句。

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

上述代码仅仅执行若干比较与赋值操作,没个操作都需要3行代码,当条件数量大增时,代码将急剧膨胀。当改为switch时,情况似乎也没有好转。

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

这样,代码变得紧凑,平均每个检查仅用一行。此外我们还可以通过谓词(predicate)来替换表达式。

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的基本能力,Vavr是基于Java 8的一个流行的函数式编程库。通过Vavr提供的功能我们可以改进我们的代码。

11. 原文地址

http://www.baeldung.com/vavr

 类似资料: