• [翻译]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

  • 相关阅读:
    Oracle之内存结构(SGA、PGA)
    Android添加快捷方式(Shortcut)到手机桌面
    Android悬浮窗实现 使用WindowManager
    Android闹钟 AlarmManager的使用
    JavaScript学习13 JavaScript中的继承
    Android Content Provider Guides
    Android存储访问及目录
    ReflectUitls类的编写和对反射机制的解析
    Java File类总结和FileUtils类
    JavaScript学习12 JS中定义对象的几种方式
  • 原文地址:https://www.cnblogs.com/snifferhu/p/6841594.html
走看看 - 开发者的网上家园