VAVR 快速指南
VAVR使用合集
导入
<dependencies>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.4</version>
</dependency>
</dependencies>
1.元组
Java 中没有直接等效的元组数据结构。元组是函数式编程语言中的一个常见概念。元组是不可变的,可以以类型安全的方式保存多个不同类型的对象。
Vavr 将元组引入 Java 8。元组的类型为Tuple1,Tuple2到Tuple8 ,具体取决于它们要采用的元素数量。
目前有八个元素的上限。我们访问像元组**._n这样的元组的元素,其中n类似于数组中索引的概念:
1.1 映射元组组件
下面是一个如何创建一个包含 String 和 Integer 的元组的示例:
Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
String s = java8._1;
Integer i = java8._2;
-
元组是通过静态工厂方法创建的
Tuple.of()
-
获取此元组的第一个元素。
-
获取此元组的第二个元素。
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 增添趣味。
就像元组一样,这些函数式接口是根据它们接受的参数数量来命名的:Function0、Function1、Function2等。使用 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。Vavr的Option实现了*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 提供了函数式接口,这些接口具有抛出检查异常的函数。这些函数是CheckedFunction0
、CheckedFunction1
等等,直到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()));
}