原文地址: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

[翻译]Javaslang 介绍的更多相关文章

  1. 《Effective Modern C++》翻译--简单介绍

    北京时间2016年1月9日10:31:06.正式開始翻译.水平有限,各位看官若有觉得不妥之处,请批评指正. 之前已经有人翻译了前几个条目,有些借鉴出处:http://www.cnblogs.com/m ...

  2. 【翻译】介绍 ASP.NET Core 中的 Razor Pages

    介绍 ASP.NET Core 中的 Razor Pages 原文地址:Introduction to Razor Pages in ASP.NET Core         译文地址:介绍 asp. ...

  3. [翻译]lithium介绍

    什么是li3? 首创框架 li3 是第一个并且是唯一一个从PHP 5.3+建立起来的相当出色的php框架,而且破天荒的第一次引入全新技术,包括通过一组唯一,统一的api(接口)在关系型(relatio ...

  4. [转]强悍的跨平台开源多媒体中心XBMC介绍

    [转]强悍的跨平台开源多媒体中心XBMC介绍 http://www.cnblogs.com/mythou/p/3220898.html 最近都在了解Android下的音视频,因为最近需要做一个多媒体中 ...

  5. Google翻译

    本博文的主要内容有 .Google翻译的介绍    .Google翻译之一:内容板块的翻译    .Google翻译之二:网页的翻译  .Google翻译之四:文档的翻译    .Google翻译之四 ...

  6. 强悍的跨平台开源多媒体中心XBMC介绍

    强悍的跨平台开源多媒体中心XBMC介绍 最近都在了解Android下的音视频,因为最近需要做一个多媒体中心的APP,其中了解了一个开源项目XMBC,一个十分强大的开源多媒体中心,而且可以应用在多个平台 ...

  7. 【机器学习Machine Learning】资料大全

    昨天总结了深度学习的资料,今天把机器学习的资料也总结一下(友情提示:有些网站需要"科学上网"^_^) 推荐几本好书: 1.Pattern Recognition and Machi ...

  8. .NET平台开源项目速览(3)小巧轻量级NoSQL文件数据库LiteDB

    今天给大家介绍一个不错的小巧轻量级的NoSQL文件数据库LiteDB.本博客在2013年也介绍过2款.NET平台的开源数据库: 1.[原创]开源.NET下的XML数据库介绍及入门 2.[原创]C#开源 ...

  9. DIV+CSS规范命名大全

    网页制作中规范使用DIV+CSS命名规则,可以改善优化功效特别是团队合作时候可以提供合作制作效率,具体DIV CSS命名规则CSS命名大全内容篇. 常用DIV+CSS命名大全集合,即CSS命名规则 D ...

随机推荐

  1. 移植ok6410 LCD驱动

    1.本次移植过程选择 linux-2.6.28 lcd驱动为参考移植到 linux-2.6.34 ok6410 开发板上. 2.移植过程 主要以给内核增加驱动的思想,在/driver/video/ 下 ...

  2. Redis安装完后redis-cli无法使用(redis-cli: command not found)已使用

    wget http://download.redis.io/redis-stable.tar.gz(下载redis-cli的压缩包) tar xvzf redis-stable.tar.gz(解压) ...

  3. 02_数据库基础之(二)sql语句入门

    1.基本增删改查操作 #一. 数据类型 常用的3中 # .字符串 例如:你的名字 我是中国人 在数据库中要使用 ‘’引起来 '苹果手机' # .整数类型 例如: 你的年龄 ,办公室的人数 个 ,直接使 ...

  4. ES5新增数组方法every()、some()、filter()、map()

    JavaScript ES5标准中新增了一些Array方法,如every().some().filter().map().它们的出现使我们能够更加便利地操作数组,但对IE9以下浏览器的兼容性比较差.下 ...

  5. metasploit framework(四):生成payload

    RHOST是限制来源IP,这里不用修改 generate生成payload 假设'\x00'是坏字符,生成payload时就会对'\x00'进行编码,排除坏字符. -b 去掉坏字符 -t 指定生成的格 ...

  6. day25 面向对象之多态和鸭子类型

    1.封装方法 如何封装:给方法名称前面加上双下划线 # ATM 的取款功能 # 1.插入银行卡 2.输入密码 3.选择取款金额 4.取款 class ATM: def __insert_card(se ...

  7. java 1.8

    rpm -qa|grep java (列出本机已安装的java,没有则没空)rpm -e --nodeps 文件名(上一步查到的文件名,一个一个复制过来卸载就好.) 下载java包 https://w ...

  8. python中matplotlib 的简单使用

    1.简单折线图的画图,轴标签.图的颜色,风格,等等参数,本文只介绍最常用的几个参数: import matplotlib.pyplot as plt import numpy as np x = np ...

  9. 关于cdh 5.X 的agent 客户端镜像安装注意事项

    当把客户端镜像安装时,每个客户端程序会生成UUID作为唯一标识,重新 安装时要删除 rm -rf /var/lib/cloudera-scm-agent 如果不删除会造成主机列表中IP一直在变的情况.

  10. rancher2 挂载ceph-rbd

    一-创建ceph-secret.yml文件 RBD的KEY查看 ceph auth list 复制 client.admin 的key 并使用base64加密 创建ceph-secret.yml ku ...