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

目录

1、概述

1.1、Maven依赖

2、Option

3、元组Tuple

4、Try

5、函数式接口

7、验证Validation

8、延迟计算Lazy

9、模式匹配Pattern Matching

10、总结

11、原文地址


1、概述

在本文中,我们将准确研究Vavr,为什么需要它以及如何在我们的项目中使用它。Vavr是Java 8+的函数库,提供不可变数据类型和功能控制结构。

1.1、Maven依赖

要使用Vavr,您需要添加依赖项:

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

建议始终使用最新版本。您可以通过以下链接获取它。

2、Option

Option的主要目标是通过利用Java类型系统来消除代码中的空值检查。

Option是Vavr中的一个对象容器,其最终目标类似于Java 8中的Optional .Vavr的Option实现了Serializable,Iterable,并且具有更丰富的API 

由于Java中的任何对象引用都可以具有值,因此我们通常必须在使用if语句之前使用if语句检查是否为null。这些检查使代码健壮且稳定:

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

没有检查,应用程序可能会因简单的NPE而崩溃

@Test(expected = NullPointerException.class)
public void givenValue_whenNullCheckNeeded_thenCorrect2() {
    Object possibleNullObj = null;
    assertEquals("somevalue", possibleNullObj.toString());
}

但是,检查会使代码变得冗长且不易读取,尤其是当if语句最终嵌套多次时。Option通过完全消除空值并用每个可能方案的有效对象引用替换它们来解决此问题。使用Option时,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实例中,如上所示。

请注意,我们在调用toString之前不必进行检查,但我们不必像以前那样处理NullPointerException。Option的toString在每次调用中返回有意义的值。

在本节的第二个片段中,我们需要一个空检查,在尝试使用它之前,我们将为变量分配一个默认值。Option可以在一行中处理这个问题,即使有一个null:

@Test
public void givenNull_whenCreatesOption_thenCorrect() {
    String name = null;
    Option<String> nameOption = Option.of(name);
    
    assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}

或者非空:

@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,Tuple2为Tuple8,具体取决于它们要采用的元素数量。

目前有八个元素的上限。我们访问元组的元素,如元组._n,其中n类似于数组中索引的概念:

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

请注意,第一个元素是使用n == 1检索的。所以元组不像数组一样使用零基。将存储在元组中的元素类型必须在其类型声明中声明,如上下文所示:

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

元组的位置是存储一组固定的任何类型的对象,这些对象作为一个单元被更好地处理并且可以被传递。更明显的用例是从 Java中的函数或方法返回多个对象

4、Try

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

以下面的代码为例:

@Test(expected = ArithmeticException.class)
public void givenBadCode_whenThrowsException_thenCorrect() {
    int i = 1 / 0;
}

没有try-catch块,应用程序就会崩溃。为了避免这种情况,您需要将语句包装在try-catch块中。使用Vavr,我们可以在Try实例中包含相同的代码并获得结果:

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

在上述所有情况下,由于Vavr的尝试,我们可以控制计算后发生的事情。

5、函数式接口

随着Java 8的到来,函数式接口内置且易于使用,尤其是与lambdas结合使用时。

但是,Java 8仅提供两个基本功能。一个只需要一个参数并产生一个结果:

@Test
public void givenJava8Function_whenWorks_thenCorrect() {
    Function<Integer, Integer> square = (num) -> num * num;
    int result = square.apply(2);
 
    assertEquals(4, result);
}

第二个只接受两个参数并产生一个结果:

@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进一步扩展了Java中函数式接口的概念,最多支持八个参数,并通过memoization,composition和currying方法调整API。

就像元组一样,这些函数式接口根据它们采用的参数数量命名:Function0Function1Function2等。使用Vavr,我们可以编写上面这两个函数:

@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中我们需要使用Consumer类型,在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与任何函数结合起来,从方法引用创建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

Vavr团队在设计满足函数式编程要求(即持久性,不变性)的新集合API方面投入了大量精力。

Java集合是可变的,使它们成为程序失败的重要来源,尤其是在存在并发的情况下。该系列接口提供了一些方法,如这样的:

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

此方法删除集合中的所有元素(产生副作用)并且不返回任何内容。,因此有了诸如ConcurrentHashMap这样的类。

这样的类不仅增加了零边际效益,而且降低了它试图填补其漏洞的类的性能。

通过不变性,我们可以免费获得线程安全:无需编写新类来处理首先不应存在的问题。

在Java中为集合添加不变性的其他现有策略仍然会产生更多问题,即异常:

@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中创建列表:

@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 Collections Framework中的大多数常见类,并且实现了其所有特征。Vavr提供的集合工具使得编写的代码更加紧凑,健壮,并且提供了丰富的功能。

7、验证Validation

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

public class Person {
    private String name;
    private int age;
 
    // standard constructors, setters and getters, toString
}

接下来,我们创建一个名为PersonValidator的类。每个字段将由一个方法验证,另一个方法可用于将所有结果合并到一个验证实例中:

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包含了错误。因此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检查状态时,发现函数并没有被执行。在第三行代码中,我们通过调用Lazy.get来表示对计算值的兴趣。此时,函数执行,Lazy.evaluated返回true。
我们也继续尝试再次获取值来确认Lazy的memoization位。如果我们提供的功能再次执行,我们肯定会收到一个不同的随机数。
然而,Lazy再次懒惰地返回最初计算的值,因为最终断言确认。

9、模式匹配Pattern Matching

模式匹配是几乎所有函数式编程语言中的基本概念。现在Java中没有这样的东西。

相反,每当我们想要执行计算或根据我们收到的输入返回值时,我们使用多个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);
}

我们可以突然看到跨越多行的代码,同时只检查三种情况。每张支票都占用了三行代码。如果我们必须检查一百个案例,那将是大约300行,不好!

另一种方法是使用switch语句:

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

没有更好的。我们仍然每次检查平均3行。很多混乱和潜在的错误。忘记break子句在编译时不是问题,但可能导致以后难以检测到的bug。

在Vavr中,我们用Match方法替换整个开关块。每个caseif语句都由Case方法调用替换。

最后,像$()这样的原子模式会替换条件,然后条件会计算表达式或值。我们还将此作为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);
}

请注意代码的紧凑程度,每次检查平均只有一行。模式匹配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);
    }))
);

有些用户可能更熟悉速记版本(-v)而有些用户可能更熟悉完整版本(-version)。一个好的设计师必须考虑所有这些情况。

在不需要多个if语句的情况下,我们已经处理了多个条件。我们将在另一篇文章中详细了解模式匹配中的谓词,多个条件和副作用。

10、总结

在本文中,我们介绍了Vavr,它是Java 8的流行函数式编程库。我们已经解决了我们可以快速适应以改进代码的主要功能。

Github项目中提供了本文的完整源代码。

11、原文地址

https://www.baeldung.com/vavr

 

 

没有更多推荐了,返回首页