Java 8概念:FP,流和Lambda表达式


如果您对面向对象编程有所了解,并且一生都在使用Java(或C#,C ++),那么JDK 1.8随附的功能性编程的突然转变似乎令人困惑和令人生畏。

这个新范例真的有什么好处吗?在哪些情况下可以利用新功能?

本系列文章的目的是通过动手实例向函数编程提供一个平稳的介绍。由于Java 8附带了支持FP的一组基本构造,因此本教程还将简要概述1.8版中添加到Java的新功能,例如:

  • Lambda expressions

  • Streams

  • Optional object

为了使转换直观而轻松,最好从简单的示例开始,讨论基本用法,并熟悉新的语法。

免责声明

所以您可能在想-现在这是另一个将要转换我的人,放弃我以前编写代码的方式。不是我。我本人仍在探索FP,在每个用例中,我都尝试评估同时使用OOP,FP或两者结合使用的好处。我丝毫没有偏见,即使有一点话,我还是赞成OOP,而OOP似乎仍然更加直观和熟悉。我的目标是发现FP带来的可能性,并为您提供所有知识,以使您做出明智的选择。

一个基本的例子 我将从一个完整的例子开始。看一下它并尝试弄清楚它的作用,但是不要担心它看起来是否复杂和不直观。不久前,我有相同的印象,但是通过适当剂量的仔细分析,很容易克服。

Map < String, List < String >> phoneNumbers = new HashMap < String, List < String >> ();

phoneNumbers.put("John Lawson", Arrays.asList("3232312323", "8933555472"));
phoneNumbers.put("Mary Jane", Arrays.asList("12323344", "492648333"));
phoneNumbers.put("Mary Lou", Arrays.asList("77323344", "938448333"));

Map < String, List < String >> filteredNumbers = phoneNumbers.entrySet().stream()
    .filter(x - > x.getKey().contains("Mary"))
    .collect(Collectors.toMap(p - > p.getKey(), p - > p.getValue()));

filteredNumbers.forEach((key, value) - > {
    System.out.println("Name: " + key + ": ");
    value.forEach(System.out::println);
});

代码的第一部分很明显-我们正在创建一个映射,将一个人的名字映射到他们的电话号码列表,为了简化起见,所有这些号码都存储为字符串(不是整数是一个复杂的概念,而是…)

然后变得有点晦涩。通过方法(或函数)的名称,您可以知道对输入映射(filter(), )进行了一些过滤和迭代forEach(),但是预测实际的输出是相当困难的。更不用说为什么要使用它而不是经典方法。

让我逐行将这段代码分解给您,并说明发生了什么。

将代码从功能性转换为命令性

如果到目前为止您只使用过OOP(就像我们大多数人一样,那不是一件坏事,请不要误解我),那么您就使用了语句 -表示要执行的某些操作的命令。这些语句通常会更改程序的状态。例如,如果要递增变量...

Integer x = 0;
x++;

...那么您将覆盖其先前的状态。很简单 之所以称为命令式编程,是因为它类似于语音中的命令式语气,它告诉程序执行一些命令。

相反,您具有函数式编程,它是声明性编程范例的子集 。它还具有命令,但是将命令更像数学函数一样对待。他们的任务是接受一些输入,执行一些操作,然后返回结果。这里最重要的部分是它们不修改程序状态。输入变量保持不变,并且返回的结果始终是新变量。

这是来自JavaScript的示例:

function max(a, b) {
    return a > b ? a : b;
}

var x = 10;
var y = 5;
var maximum = max(x, y);

调用 max 函数后,变量x 和 的值 y 保持不变。该 max 函数不会修改输入,也不会依赖或修改任何外部(全局)变量。

代码分析

因此,让我们回到示例:

// create a map, filter it by key values
Map < String, List < String >> filteredNumbers = phoneNumbers.entrySet().stream()
    .filter(x - > x.getKey().contains("Mary"))
    .collect(Collectors.toMap(p - > p.getKey(), p - > p.getValue()));

让我们在这里讨论每个操作。首先,我将重点关注代码的作用,而不是代码的作用,以及从理论的角度来说,此表达式的特定构造意味着什么。

我们正在获取输入地图 phoneNumbers,以某种方式对其进行转换,还将地图作为输出。首先,我们调用该 entrySet() 方法以获得一组条目,每个条目由一个键和一个值组成。

然后,该 stream() 方法被调用。这样就创建了一个流(java.util.stream.Stream)。这是Java 8中引入的新概念,现在,让我们继续使用Javadoc提供的定义:

流是支持顺序和并行聚合操作的一系列元素。

有问题的操作可以是过滤,修改和不同类型的转换。我们的下一个功能是filter(),其目的当然是使用某些条件过滤流中的项目。标准表示为:

x -> x.getKey().contains("Mary")

这种新的语法构造称为 lambda表达式 ,它也是1.8版中引入Java的概念之一。在这种特定情况下,lambda表达式是一个谓词 -布尔函数。其目标是评估过滤条件,并确定是否应保留或删除集合(流)中的每个特定项目。条件很简单:

// get the key of the map item and check if it contains the word “Mary”
x.getKey().contains("Mary")

它获取当前处理的地图项的键,并检查其是否包含子字符串“ Mary”。如果是,则谓词(lambda表达式)返回 true ,并将该项保留在流中。

下一个方法是collect()。如您所见,方法是链接的,并且它们都属于Stream 接口,因此这表明它们每个都返回流,以供调用过程中的后续方法访问。

该 collect() 方法仅获取流的元素(类型为 java.util.Map.Entry),然后将其转换回常规集合。

到目前为止,一切都很好。我们稍后将更详细地讨论一些新概念(流,lambda表达式,谓词),但是总的来说,这些代码变得更加透明。话虽如此,我希望使用这种新方法的好处可能尚不明显,但请耐心等待。

迭代集合 现在,让我们看一下第二个片段:

filteredNumbers.forEach((key, value) - > {
    System.out.println("Name: " + key + ": ");
    value.stream().forEach(System.out::println);
});

让我们尝试使用forEach Java 8中引入的新循环,而不是使用老式循环 。它应该是可以理解的-它对基础集合的每个元素执行一些操作。再次通过lambda表达式定义应用于项目的实际操作:

// expression takes two parameters
(key, value) - > {
    // print person’s name
    System.out.println("Name: " + key + ": ");
    // iterate over the person’s phone numbers and print each of them
    value.forEach(System.out::println);
}

该表达式是一个带有两个参数的函数, key 和 value , (因为它对映射条目进行操作),并且每个对都执行两个操作。首先,它输出人名( key 参数):

System.out.println("Name: " + key + ": ");

然后,它调用另一个 forEach 函数以打印value 列表中的所有电话号码。

整个操作到控制台的输出将是:

Name: Mary Lou:
77323344
938448333
Name: Mary Jane:
12323344
492648333

这是一个冗长的分析,但是如果您现在有了主意,则以后再也不必回到基础知识了。


原文链接:http://codingdict.com