VAVR 快速指南

VAVR 快速指南

VAVR使用合集

导入

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

1.元组

Java 中没有直接等效的元组数据结构。元组是函数式编程语言中的一个常见概念。元组是不可变的,可以以类型安全的方式保存多个不同类型的对象。

Vavr 将元组引入 Java 8。元组的类型为Tuple1,Tuple2Tuple8 ,具体取决于它们要采用的元素数量。

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

1.1 映射元组组件

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

       Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
        String s = java8._1;
        Integer i = java8._2;
  1. 元组是通过静态工厂方法创建的Tuple.of()

  2. 获取此元组的第一个元素。

  3. 获取此元组的第二个元素。

1.2 映射元组组件

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

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

1.3 使用一个映射器映射一个元组

Transform 根据元组的内容创建一个新类型。

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

2. 功能接口

随着 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 中函数式接口的概念,最多支持八个参数,并使用记忆、组合和柯里化的方法为 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 中我们需要使用Supplier类型,在 Vavr Function0中可以提供帮助:

@Test
public void whenCreatesFunction_thenCorrect0() {
    Function0<String> getClazzName = () -> this.getClass().getName();
    String clazzName = getClazzName.apply();

    assertEquals("com.baeldung.vavr.VavrTest", clazzName);
}

我们还可以将任何函数的静态工厂方法FunctionN.of结合起来,从方法引用中创建 Vavr 函数。就像我们有以下sum方法:

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

3. Option

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

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

由于 Java 中的任何对象引用都可以具有null值,因此我们通常必须在使用if语句之前检查它是否为 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());
}

4. Try

在 Vavr 中,Try* **是可能导致异常***的计算容器。**

由于Option包装了一个可为空的对象,因此我们不必使用if检查显式处理空值,而**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 的Try

try...catch的一种实现方式

/**
* 输出
*	 failure: / by zero
*  finally
*/
Try.of(() -> 1 / 0)
  //正确执行
    .andThen(r -> System.out.println("and then " + r))
  //错误执行
    .onFailure(error -> System.out.println("failure" + error.getMessage()))
    .andFinally(() -> {
      System.out.println("finally");
});

recoverWith 方法,我们可以很优雅的实现降级策略

/**
* 输出
* NPE
* IllegalState
* Unknown
*/
@Test
public void whenTry() {
    System.out.println(testTryWithRecover(new NullPointerException()));
    System.out.println(testTryWithRecover(new IllegalStateException()));
    System.out.println(testTryWithRecover(new RuntimeException()));
}

private String testTryWithRecover(Exception e) {
    return (String) Try.of(() -> {
                throw e;
            })
            .recoverWith(NullPointerException.class, Try.of(() -> "NPE"))
            .recoverWith(IllegalStateException.class, Try.of(() -> "IllegalState"))
            .recoverWith(RuntimeException.class, Try.of(() -> "Unknown"))
            .get();
}

使用 map 对结果进行转换,并且与 Option 进行交互

@Test
public void testTryMap() {
    String res = Try.of(() -> "hello world")
            .map(String::toUpperCase)
            .toOption()
            .getOrElse(() -> "default");
    System.out.println(res);
}

5. Lazy

Lazy是一个容器,它表示一个延迟计算的值,即延迟计算直到需要结果。此外,评估值被缓存或记忆,并在每次需要时一次又一次地返回,而无需重复计算:

@Test
public void givenFunction_whenEvaluatesWithLazy_thenCorrect() {
    Lazy<Double> lazy = Lazy.of(Math::random);
    //检查是否评估了此惰性值 通过调用get()  如果该值被评估,则为 true,否则为 false。
    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.evaluate返回 true。

我们再次获取来确认*Lazy的记忆位。*如果我们提供的函数再次执行,我们肯定会收到一个不同的随机数。

然而,当最终断言确认时, Lazy再次延迟返回最初计算相同的值。

6. 模式匹配

模式匹配是几乎所有函数式编程语言中的原生概念。目前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子句在编译时不是问题,但可能会导致以后难以检测到错误。
最后,像$()这样的原子模式替换了然后评估表达式或值的条件。我们还将它作为第二个参数提供给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);
}

例如,我们可以用谓词替换原子表达式。想象一下,我们正在解析一个控制台命令以获取帮助版本标志:一些用户可能更熟悉简写版本 (-v),而另一些用户则更熟悉完整版本 (-version)。一个好的设计师必须考虑所有这些情况。

不需要多个if语句,我们已经处理了多个条件。

@Test
public void whenMatchworks_thenCorrect() {
    String arg="--help";
    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);
            }))
    );
}
private void displayVersion(){
    System.out.println("--version");
}

private void displayHelp(){
    System.out.println("--help");
}

6.1 穷举

最后一个通配符模式$()使我们免于大小写不匹配时抛出的 MatchError。

因为我们不能像 Scala 编译器那样执行详尽检查,所以我们提供了返回可选结果的可能性:

Integer i = 3;
Option<String> s = Match(i).option(
        Case($(0), "zero")
);
System.out.println(s.isEmpty());

6.2 句法糖

import static io.vavr.Predicates.*;

//is用来来测试一个对象是否等于指定的value ,使用Objects.equals(Object, Object)进
String str = Match(i).of(
         Case($(is(1)), "one"),
         Case($(is(2)), "two"),
         Case($(), "?")
);
System.out.println(str);
//使用isIn谓词来检查多个条件
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);
    }))
);

6.3 类型匹配

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

6.4 Try

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

import static io.vavr.Patterns.*;
  • Success:代表执行没有异常
  • Failure:代表执行出现异常
var res = Try.of(() -> "Nice");
String of = Match(res).of(
        // 匹配任意成功的情况
        Case($Success($()), r -> "Nice"),
        // 匹配任意失败的情况
        Case($Failure($()), r -> "fail")
);
System.out.println(of);
var res = Try.of(() -> {
    throw new RuntimeException();
});
Object of = Match(res).of(
        // 匹配成功情况
        Case($Success($()), r -> run(Assert::fail)),
        // 匹配异常为 RuntimeException
        Case($Failure($(instanceOf(RuntimeException.class))), r -> true),
        // 匹配异常为 IllegalStateException
        Case($Failure($(instanceOf(IllegalStateException.class))), r -> run(Assert::fail)),
        // 匹配异常为 NullPointerException
        Case($Failure($(instanceOf(NullPointerException.class))), r -> run(Assert::fail)),
        // 匹配其余失败的情况
        Case($Failure($()), r -> run(Assert::fail))
);
System.out.println(of);

6.5 Option

  • Some: 代表有值
  • None: 代表没有值
import static io.vavr.Patterns.*;

Option option = Option.of("defined");
String of = Match(option).of(
        Case($Some($()), "defined"),
        Case($None(), "empty")
);
System.out.println(of);

6.6 Tuple

var tup = Tuple.of("hello", 2);
// 模式匹配
Match(tup).of(
        Case($Tuple2($(is("hello")), $(is(1))), (t1, t2) -> run(() -> {
            System.out.println(true);
        })),
        Case($Tuple2($(), $()), (t1, t2) -> run(() -> {
            System.out.println(false);
        }))
);

6.7 isNull

@Test
public void givenInput_whenMatchesNull_thenCorrect() {
    Object obj=5;
    String s = Match(obj).of(
      Case($(isNull()), "no value"), 
      Case($(isNotNull()), "value found"));

    assertEquals("value found", s);
}

6.8 allOf

可以使用allOf谓词进行AND运算

    @Test
    public void givenInput_whenMatchAllWorks_thenCorrect() {
        Integer i = null;
        String s = Match(i).of(
                // 不等于空,并且匹配isIn中的值 相当于 and &&
                Case($(allOf(isNotNull(),isIn(1,2,3,null))), "Number found"),
                Case($(), "Not found"));
        assertEquals("Not found", s);
    }

6.9 anyOf

可以使用anyOf谓词进行 OR 运算

假设我们按出生年份筛选候选人,我们只需要出生于 1990、1991 或 1992 年的候选人。

如果没有找到这样的候选人,那么我们只能接受 1986 年出生的人

@Test
    public void givenInput_whenMatchesAnyOfWorks_thenCorrect() {
        Integer year = 1990;
        String s = Match(year).of(
          Case($(anyOf(isIn(1990, 1991, 1992), is(1986))), "Age match"), 
          Case($(), "No age match"));
        assertEquals("Age match", s);
    }

6.10 noneOf

可以使用noneOf谓词进行 != 运算

    @Test
    public void givenInput_whenMatchesNoneOfWorks_thenCorrect() {
        Integer year = 1990;
        String s = Match(year).of(
                Case($(noneOf(isIn(1990, 1991, 1992), is(1986))), "Age match"),
                Case($(), "No age match"));
        assertEquals("No age match", s);
    }

6.11 自定义谓词

有了 lambdas 的知识,我们可以构建和使用我们自己的谓词,甚至直接将它们写成内联。

    @Test
    public void whenMatchWorksWithCustomPredicate_thenCorrect() {
        int i = 3;
        String s = Match(i).of(
                Case($(n -> n == 1), "one"),
                Case($(n -> n == 2), "two"),
                Case($(n -> n == 3), "three"),
                Case($(), "?"));
        assertEquals("three", s);
    }

如果我们需要更多参数,我们还可以应用功能接口代替谓词。contains 示例可以像这样重写,虽然有点冗长,但它让我们对谓词的作用有更大的权力:

    @Test
    public void givenInput_whenContainsWorks_thenCorrect2() {
        int i = 5;
        BiFunction<Integer, List<Integer>, Boolean> contains = (t, u) -> u.contains(t);
        String s = Match(i).of(
                Case($(o -> contains.apply(i, Arrays.asList(2, 4, 6, 8))), "Even Single Digit"),
                Case($(o -> contains.apply(i, Arrays.asList(1, 3, 5, 7, 9))), "Odd Single Digit"),
                Case($(), "Out of range"));
        assertEquals("Odd Single Digit", s);
    }

在上面的示例中,我们创建了一个 Java 8 BiFunction,它只检查两个参数之间的isIn关系。

也可以为此使用 Vavr 的FunctionN.因此,如果内置谓词与您的要求不完全匹配,或者您希望控制整个评估,则使用自定义谓词。

7. Either

Either 表示可能有两种不同类型的值,分别称为左值或右值。只能是其中的一种情况。Either 通常用来表示成功或失败两种情况。惯例是把成功的值作为右值,而失败的值作为左值。可以在 Either 上添加应用于左值或右值的计算。应用于右值的计算只有在 Either 包含右值时才生效,对左值也是同理。

@Test  
public void whenEither_thenCorrect(String[] args) {
        Either<String, String> either = compute()
                .map(str -> str + " World")
                .mapLeft(Throwable::getMessage);
        System.out.println(either);
    }

    private static Either<Throwable, String> compute() {
        ThreadLocalRandom random =
                ThreadLocalRandom.current();
        return random.nextBoolean()? Either.left(new RuntimeException("Boom!")): Either.right("Hello");
    }

8. Future

Future 是在某个时刻可用的计算结果。提供的所有操作都是非阻塞的。底层 ExecutorService 用于执行异步处理程序

Vavr通过Future简化了线程的使用方式,不用再像Java定义任务,创建线程,再执行,直接创建一个Future对象即可。Future提供的所有操作都是非阻塞的,其底层的ExecutorService用于执行异步处理程序。代码如下:

    @Test
    public void testFuture() {
        System.out.println("当前线程名称:" + Thread.currentThread().getName());
        Integer result = Future.of(() -> {
                    System.out.println("future线程名称:" + Thread.currentThread().getName());
                    Thread.sleep(2000);
                    return 100;
                })
                .map(i -> i * 10)
                .await()
                .get();
        System.out.println(result);
    }

//        这个 Future 可不是 java.util.concurrent.Future,但它们都是对异步计算结果的一个抽象。
//        vavr 的 Future 提供了比 java.util.concurrent.Future 更友好的回调机制
//
//        onFailure 失败的回调
//        onSuccess 成功的回调
@Test
public void testFutureFailure() {
    final var word = "hello world";
    io.vavr.concurrent.Future
      .of(Executors.newFixedThreadPool(1), () -> word)
      .onFailure(throwable -> Assert.fail("不应该走到 failure 分支"))
      .onSuccess(result -> Assert.assertEquals(word, result));
}

@Test
public void testFutureSuccess() {
    io.vavr.concurrent.Future
      .of(Executors.newFixedThreadPool(1), () -> {
          throw new RuntimeException();
      })
      .onFailure(throwable -> Assert.assertTrue(throwable instanceof RuntimeException))
      .onSuccess(result -> Assert.fail("不应该走到 success 分支"));
}


9. List

Vavr 团队在设计满足函数式编程要求(即持久性、不变性)的新集合 API 方面付出了很多努力。 Java 集合是可变的,使它们成为程序失败的重要来源,尤其是在存在并发的情况下。 Collection 接口提供如下方法:

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

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

    assertEquals(6, sum);
}

10. 处理stream异常

10.1.简介

JDK 提供的功能接口没有处理异常的能力

下面我们用代码实践,定义一个抛出异常方法readFromFile

    static Integer readFromFile(Integer integer) throws IOException {
        if (integer % 2 == 0) {
            throw new IllegalStateException("an integer is not even");
        }
        // logic to read from file which throws IOException
        return integer;
    }

消费这个方法可以看到我们必须要使用try-catch包装这个方法

    @Test
    public void streams() {
        List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
        integers.stream().map(i -> {
            try {
                return readFromFile(i);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

10.2.使用CheckedFunction

Vavr 提供了函数式接口,这些接口具有抛出检查异常的函数。这些函数是CheckedFunction0CheckedFunction1等等,直到CheckedFunction8函数名末尾的0 , 1, ... 8表示函数的输入参数个数。

    @Test
    public void checkedFunction() {
        List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
        CheckedFunction1<Integer, Integer> readFunction = i -> readFromFile(i);
        integers.stream().map(readFunction.unchecked()).forEach(i -> System.out.println(i));
   }

   // 输出 java.lang.IllegalStateException: an integer is not even

如你所见,没有使用try-catch包装方法,我们仍然可以在 lambda 表达式中调用异常抛出方法,在使用Stream API的这个特性时我们必须小心,因为异常会立即终止操作——放弃流的其余部分

10.3.使用API方法

与2效果一样,出现异常会立即终止操作

    @Test
    public void api() {
        List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
        integers.stream().map(API.unchecked(i -> readFromFile(i))).forEach(i -> System.out.println(i));
    }

    // 输出 java.lang.IllegalStateException: an integer is not even

10.4.使用lift

为了优雅地处理IOException ,我们可以在 lambda 表达式中引入标准的 try-catch块。但是,lambda 表达式的简洁性将会丢失。Vavr 的lift救了我们。

    @Test
    public void lift() {
        List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
        //CheckedFunction1.lift返回的是  Option<R>类型的值,所以可以用getOrElse来为异常方法设定默认值
        integers.stream().map(CheckedFunction1.lift(i -> readFromFile(i))).map(k -> k.getOrElse(-1)).forEach(i -> System.out.println(i));
    }

输出

3
9
7
-1
-1
-1

10.5.使用Try

虽然上一节中的方法lift()解决了程序突然终止的问题,但它实际上吞下了异常。因此,我们方法的使用者不知道是什么导致了默认值。另一种方法是使用Try容器。Try是一个特殊的容器,我们可以用它来封装一个可能引发异常的操作。在这种情况下,生成的Try对象代表一个失败,它包装了异常。

让我们看一下使用Try的代码:

    @Test
    public void trys() {
        List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
        //CheckedFunction1.liftTry返回的是  Try<R>类型的值 使用Value.toJavaStream将结果转成流
        integers.stream().map(CheckedFunction1.liftTry(i -> readFromFile(i))).flatMap(Value::toJavaStream).forEach(i -> System.out.println(i));
        //输出 3 9 7

        //通过Try.isFailure方法筛选出异常值,打印异常信息
        integers.stream().map(CheckedFunction1.liftTry(i -> readFromFile(i))).filter(Try::isFailure).forEach(i -> System.out.println(i.getCause()));
    }