[翻译]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。
[翻译]Javaslang 介绍的更多相关文章
- 《Effective Modern C++》翻译--简单介绍
北京时间2016年1月9日10:31:06.正式開始翻译.水平有限,各位看官若有觉得不妥之处,请批评指正. 之前已经有人翻译了前几个条目,有些借鉴出处:http://www.cnblogs.com/m ...
- 【翻译】介绍 ASP.NET Core 中的 Razor Pages
介绍 ASP.NET Core 中的 Razor Pages 原文地址:Introduction to Razor Pages in ASP.NET Core 译文地址:介绍 asp. ...
- [翻译]lithium介绍
什么是li3? 首创框架 li3 是第一个并且是唯一一个从PHP 5.3+建立起来的相当出色的php框架,而且破天荒的第一次引入全新技术,包括通过一组唯一,统一的api(接口)在关系型(relatio ...
- [转]强悍的跨平台开源多媒体中心XBMC介绍
[转]强悍的跨平台开源多媒体中心XBMC介绍 http://www.cnblogs.com/mythou/p/3220898.html 最近都在了解Android下的音视频,因为最近需要做一个多媒体中 ...
- Google翻译
本博文的主要内容有 .Google翻译的介绍 .Google翻译之一:内容板块的翻译 .Google翻译之二:网页的翻译 .Google翻译之四:文档的翻译 .Google翻译之四 ...
- 强悍的跨平台开源多媒体中心XBMC介绍
强悍的跨平台开源多媒体中心XBMC介绍 最近都在了解Android下的音视频,因为最近需要做一个多媒体中心的APP,其中了解了一个开源项目XMBC,一个十分强大的开源多媒体中心,而且可以应用在多个平台 ...
- 【机器学习Machine Learning】资料大全
昨天总结了深度学习的资料,今天把机器学习的资料也总结一下(友情提示:有些网站需要"科学上网"^_^) 推荐几本好书: 1.Pattern Recognition and Machi ...
- .NET平台开源项目速览(3)小巧轻量级NoSQL文件数据库LiteDB
今天给大家介绍一个不错的小巧轻量级的NoSQL文件数据库LiteDB.本博客在2013年也介绍过2款.NET平台的开源数据库: 1.[原创]开源.NET下的XML数据库介绍及入门 2.[原创]C#开源 ...
- DIV+CSS规范命名大全
网页制作中规范使用DIV+CSS命名规则,可以改善优化功效特别是团队合作时候可以提供合作制作效率,具体DIV CSS命名规则CSS命名大全内容篇. 常用DIV+CSS命名大全集合,即CSS命名规则 D ...
随机推荐
- JS----addEventListener()
addEventListener() 用于向指定元素添加事件. 可以向一个元素添加多次事件或者多次不同事件,后面的事件是不会覆盖前面的. 语法: element.addEventListener(ev ...
- Error logged from Ant UI:
2down vote Have a look at the Eclipse error log. I recently saw following error there: Error logged ...
- kali下的miranda工具只适合同一路由下使用
在终端输入如下命令: miranda -v -i eth0 上面的命令是指定打开网卡eth0,返回结果如下: miranda提示输入开启upnp的主机,现在我们不知道哪台主机开启了upnp,输入命令“ ...
- mongo副本集设置主库权重,永远为主
mongo副本集设置主库权重,即使主库宕机了再重启也还是主库. cfg = rs.conf() ------->(查看序列)cfg.members[0].priority = 1 (设置 ...
- 使用synchronized wait() notifyall() 实现简单的加减法同步 竞争抢答
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.co ...
- jQuery 设置/获取样式
参考 http://www.w3school.com.cn/jquery/jquery_css.asp $("#a").css("height"); $(&qu ...
- 动态规划 51nod 1183
题目链接:http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1183 1183 编辑距离 基准时间限制:1 秒 空间限制:1 ...
- 打印信息,通过.jasper工具将集合输出到PDF文件 然后利用打印机打印文件
我们上一次成功的利用iReport工具制作了一张报表,并且预览了报表最后的效果,也生成了格式为“jrpxml”.“jrxml”与“jasper”的文件.这次,我们使用jasper提供的java的api ...
- Jmeter录制APP脚本
启动 jmeter.bat 在 Test Plan 下 添加 Thread Group 在 WorkBench 下 添加 HTTP(S) Test Script Recorder: 配置 Global ...
- TZOJ 3481 Highway Construction(树的直径+最短路)
描述 As head of the Accessible Commuting Movement (ACM), you've been lobbying the mayor to build a new ...