1 概述
Vavr 是Java 8+中一个函数式库,提供了一些不可变数据类型及函数式控制结构。
1.1 Maven 依赖
添加依赖,可以到maven仓库中查看最新版本。
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.9.0</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提供的功能我们可以改进我们的代码。