[翻译]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 实现了 Serializable,Iterable接口,并且有着丰富的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方法之前,没有做任何检查,然而并没有报NullPointerException 。Option 每次调用toString方法返回值都是有效、可用的值。
在本章节的第二部分,再展示一个null 校验的示例。
我们在使用变量name之前,为name分配一个默认值,然后再尝试使用它。
即使name为null ,Option 仅需一行处理:
@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。元组类型有Tuple1,Tuple2到Tuple8,具体则取决于它的元素数量。
目前为止的极限为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(函数式接口)的命名也是通过元素数量而来:Function0、Function1、Function2以此类推。使用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 语句块。所有case 或if 语句块被替代为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 不仅仅止步于此,它还能做更复杂的处理。
例如,我们可以用谓语替换原子表达式。想象一下,我们正在解析一个控制台命令以获取help 和version :
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。
浮生潦草闲愁广,一听啤酒一口尽
 
                    
                 
                
            
         
 浙公网安备 33010602011771号
浙公网安备 33010602011771号