潦草白纸

[翻译]Javaslang 介绍

原文地址:Introduction to Javaslang

1. 概述


在这篇文章中,我们将会探讨:

  • Javaslang 是什么?
  • 为什么需要它?
  • 以及怎样在项目中使用它?

Javaslang 是Java 8+的函数式工具库,提供了不变数据类型和函数式语法结构。

1.1 Maven 依赖

为了使用Javaslang,您需要添加依赖关系:

<dependency>
    <groupId>io.javaslang</groupId>
    <artifactId>javaslang</artifactId>
    <version>2.1.0-alpha</version>
</dependency>
    

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

2. Option


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

在Javaslang 中Option 是一个类似于 Java 8里的Optional 的对象容器。Javaslang的 Option 实现了 SerializableIterable接口,并且有着丰富的API。

在Java 中,任何对象引用,都可能是null 值。我们常常不得不在使用对象前,通过if 语句校验它是否为null 。这些校验使得代码更健壮、更稳定。

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

没有检查,应用程序可能因为简单的NPE(NullPointException) 而崩溃。

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

不管结果如何,这些检查总是使得代码变得冗长、难以阅读,特别是当这些if 语句被嵌套多次的时候。

Option 针对每个对应的场景,将其替换为有效对象引用,完全消除了null 值,从而解决了此问题。

利用Option 处理null 值,它将会被转义为一个None 实例;
当遇到非空值,它将会被转义为一个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方法之前,没有做任何检查,然而并没有报NullPointerExceptionOption 每次调用toString方法返回值都是有效、可用的值。

在本章节的第二部分,再展示一个null 校验的示例。
我们在使用变量name之前,为name分配一个默认值,然后再尝试使用它。
即使namenullOption 仅需一行处理:

@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 中并没有直接的元组数据类型。在函数式编程语言里,元组是一种常见的概念。元组是不可变类型,并且能安全的容纳多种不同类型的对象。

Javaslang 将元组引入到了Java 8。元组类型有Tuple1Tuple2Tuple8,具体则取决于它的元素数量。

目前为止的极限为8个元素。我们访问元组的元素,就像数组根据下标获取元素一样,如:tuple._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。所以元组并不像数组一样,使用0作为基数。

将要存储到元组中的元素类型顺序,必须依据元组类型声明的类型顺序。如下所示:

@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


在Javaslang 中,Try 是用于计算的容器,可能返回异常。

就像Option 的包装可能为空的对象一样,我们不再需要使用if 来做null 值校验。Try 包装了一个计算,这样我们就不再需要使用try-catch块来处理异常。

请看以下代码示例:

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

缺少了try-catch块,程序将会崩溃。为了避免这种情况,你需要包装这段语句到try-catch块中。

使用Javaslang,我们可以在Try 实例中包装相同的代码(1 / 0)并获取结果:

@Test
public void givenBadCode_whenTryHandles_thenCorrect() {
    Try<Integer> result = Try.of(() -> 1 / 0);
 
    assertTrue(result.isFailure());
}

无论计算是否成功,都可以在代码中的任何位置通过判断isFailure,决定下一步的处理。

在之前的代码段中,我们展示了一个成功或失败的简单校验。

  • 在此展示使用默认返回值的示例:
@Test
public void givenBadCode_whenTryHandles_thenCorrect2() {
    Try<Integer> computation = Try.of(() -> 1 / 0);
    int result = result.getOrElse(-1);
 
    assertEquals(-1, result);
}
  • 选择声明抛出异常的示例:
@Test(expected = ArithmeticException.class)
public void givenBadCode_whenTryHandles_thenCorrect3() {
    Try<Integer> result = Try.of(() -> 1 / 0);
    result.getOrElseThrow(ArithmeticException::new);
}

综上所述,感谢Javaslang 的Try ,让我们可以更简洁、方便的控制计算之后,所需采取的应对措施。

5. Functional Interfaces(函数式接口)


随着Java 8的到来,functional interfaces(函数式接口)被内建,同时使用起来也比较方便,特别是与lambda表达式配合使用。

但是,Java 8 只提供了两个基础的functional interfaces(函数式接口)。

  • 一个只能传入单一参数并返回一个结果:
@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);
}

另一方面,Javaslang 通过最多支持八个参数来扩展Java 中的functional interfaces(函数式接口)的概念,将memoization(备忘录模式)composition(函数组合)curry(柯里化)的概念乱炖入API和方法中。

就像是tuples(元组)一样,这些functional interfaces(函数式接口)的命名也是通过元素数量而来:Function0Function1Function2以此类推。使用Javaslang ,我们可以重写上面的两个示例方法如下:

  • 一个参数
@Test
public void givenJavaslangFunction_whenWorks_thenCorrect() {
    Function1<Integer, Integer> square = (num) -> num * num;
    int result = square.apply(2);
 
    assertEquals(4, result);
}
  • 两个参数
@Test
public void givenJavaslangBiFunction_whenWorks_thenCorrect() {
    Function2<Integer, Integer, Integer> sum = 
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);
 
    assertEquals(12, result);
}
  • 当不需要参数,但仍然需要一个输出结果时

在Java 8 中我们需要使用Consumer类型,在Javaslang 中Function0同样可以提供帮助:

@Test
public void whenCreatesFunction_thenCorrect0() {
    Function0<String> getClazzName = () -> this.getClass().getName();
    String clazzName = getClazzName.apply();
 
    assertEquals("com.baeldung.javaslang.JavaSlangTest", clazzName);
}
  • 就算需要5个参数

这只是使用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 ", "Javaslang");
 
    assertEquals("Hello world! Learn Javaslang", finalString);
}
  • 还可以利用静态工厂方法FunctionN.of

组合任何方法引用,创建为Javaslang 函数。就像是我们有着如下的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);
}

6. Conllections(集合)


Javaslang 团队投入了大量精力来设计一套新的集合类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");
}

所有之前所提及的问题,在Javaslang的集合类型中都不复存在。

在Javaslang 中创建一个列表:

@Test
public void whenCreatesJavaslangList_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));
}

在列表中,同时可以通过APIs,来实现计算:

@Test
public void whenSumsJavaslangList_thenCorrect() {
    int sum = List.of(1, 2, 3).sum().intValue();
 
    assertEquals(6, sum);
}

Javaslang 集合提供了Java 集合框架中常见的类,并且实现了所有的功能。

这些API带了不变性、删除void返回,以及规避了副作用。与Java 集合操作相比,有着更加丰富的基础元素函数式操作,使得代码更加简洁、健壮、紧凑。

Javaslang 集合的全面介绍,超出了本文所要探讨的范围。

7. Validation


Javaslang 将函数式编程世界中的Applicative Functor (应用函子)概念引入到了Java 中。Applicative Functor (应用函子)能够使我们在执行一系列操作的同时累积结果。

javslang.control.Validation类有助于错误信息的收集。要记得,通常情况下程序在遇到错误时便终止了。

但是在项目中,使用Validation能使得处理继续进行,并且收集到所有异常信息,一切就像是批处理一样。

考虑下,我们按照姓名年龄 注册用户这个场景。
我们首先需要输入所有参数,然后决定是创建一个Person实例,还是返回一个错误列表。以下为我们的Person类:

public class Person {
    private String name;
    private int age;
 
    // 标准的构造方法、set和get方法、以及toString方法
}

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

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

年龄 的校验规则是它应该是一个大于0的整数,姓名 的校验规则是不应该包含任何特殊字符:

@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.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。请注意,第四行,我们检查Lazy.evaluated该值,并意识到该函数并未执行。这是因为我们还没有展示出对于返回价值的兴趣。

在第六行代码中,我们通过调用Lazy.get来显示对于计算值的兴趣。此时,函数执行Lazy.evaluated,返回true

我们通过再此尝试Lazy.get来确认Lazy的记录位。如果我们提供的方法被再次调用,我们将会获取一个不同的随机数。

然而,Lazy再次懒惰地返回最初计算的值,断言assertEquals(val1, val2, 0.1);确认为true

9. Pattern Matching(模式匹配)


Pattern Matching(模式匹配)是几乎所有函数式编程语言中的native concept(本地概念)。现在Java 中并没有这样的东西。

相反,每当我们要执行计算或返回一个基于我们收到的输入值时,我们使用多个if 语句来分解逻辑,使代码正确执行:(译者注:在java中,其实常常采用map作为逻辑分解容器,策略模式)

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

然而并没有任何改进,我们依旧需要每个校验占用三行。这样带来了很多混乱和潜在的bug 。在编译时忘记了某个break 子句并不会报错,但是在运行时将会导致难以定位的bug

在Javaslang中,我们使用Match 方法替代整个switch 语句块。所有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);
}

请注意代码的紧凑读,每一行代码则为一个校验。Pattern Matching(模式匹配)API 不仅仅止步于此,它还能做更复杂的处理。

例如,我们可以用谓语替换原子表达式。想象一下,我们正在解析一个控制台命令以获取helpversion

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. 总结


在这篇文章中,我们介绍了Javaslang,这一个Java 8的流行函数式编程库。我们已经讲解了那些可以快速适应、改进代码的主要特性。

本文展示的所有源代码都在Github

posted on 2017-05-12 09:57  潦草白纸  阅读(1764)  评论(0编辑  收藏  举报

导航