赞
踩
在本教程中,我们将了解函数式编程范式的核心原则以及如何在 Java 编程语言中使用它们。
我们还将介绍一些高级函数式编程技术。这将帮助我们了解 Java 中的函数式编程的好处。
基本上,函数式编程是一种编程风格,它将计算看作为是数学函数的求值。
在数学中,函数是将输入集与输出集相关联的表达式。函数的输出仅取决于其输入。更有趣的是,我们可以将两个或多个函数组合在一起得到一个新函数。
要理解为什么数学函数的这些定义和属性在编程中很重要,我们需要回顾一下历史。
在 1930 年代,数学家 Alonzo Church 开发了一个 formal 系统来表达基于函数抽象的计算。这种通用的计算模型后来被称为 lambda 演算。
Lambda 演算对编程语言理论的发展产生了巨大的影响,特别是函数式编程语言。一般来说,函数式编程语言实现 lambda 演算。
由于 lambda 演算专注于函数组合,函数式编程语言提供了在函数组合中组合软件的表达方式。
当然,函数式编程并不是实践中唯一的编程风格。从广义上讲,编程风格可以分为命令式和声明式编程范式。
命令式方法将程序定义为一系列语句,这些语句改变程序的状态,直到它达到最终状态。
过程式编程是一种命令式编程,我们使用过程或子例程构造程序。面向对象编程 (OOP) 扩展了过程编程概念。
相比之下,声明性方法表达了计算的逻辑,而不用一系列语句来描述其控制流。
简单地说,声明式方法的重点是定义程序必须实现什么,而不是它应该如何实现。函数式编程是声明式编程语言的一个子集。
这些类别还有更多的子类别,分类法变得相当复杂,但我们不会在本教程中深入讨论。
现在我们将尝试了解编程语言是如何根据它们对函数式编程的支持来划分的。
纯函数式语言,例如 Haskell,只允许纯函数式程序。
其他语言同时允许函数式和过程式程序,并被认为是不纯的函数式语言。许多语言都属于这一类,包括 Scala,Kotlin 和 Java。
重要的是要了解,当今大多数流行的编程语言都是通用语言,因此它们倾向于支持多种编程范式。
本节将介绍函数式编程的一些基本原则以及如何在 Java 中采用它们。
下面的例子,我们将使用Java 8
如果将函数视为一等公民,则称编程语言具有一等函数。
这意味着允许函数支持其他实体通常可用的所有操作。其中包括将函数分配给变量,将它们作为参数传递给其他函数并将它们作为值从其他函数返回。
这个属性使得在函数式编程中定义高阶函数成为可能。高阶函数能够接收函数作为参数并返回函数作为结果。这进一步实现了函数式编程中的多种技术,例如函数组合和柯里化。
传统上,在 Java 中只能使用函数式接口或匿名内部类等结构来传递函数。函数式接口只有一种抽象方法,也称为单一抽象方法 (SAM) 接口。
假设我们必须为 Collections.sort
方法提供一个自定义比较器:
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer n1, Integer n2) {
return n1.compareTo(n2);
}
});
正如我们所看到的,这是一种乏味而冗长的技术——当然这不是鼓励开发人员采用函数式编程的原因。
幸运的是,Java 8 带来了许多新特性来简化这个过程,例如 lambda 表达式、方法引用和预定义的函数式接口。
让我们看看 lambda 表达式如何帮助我们完成相同的任务:
Collections.sort(numbers, (n1, n2) -> n1.compareTo(n2));
这肯定更简洁易懂。
但是,请注意,虽然这可能会给我们一种在 Java 中使用函数作为一等公民的印象,但事实并非如此。
在 lambda 表达式的语法糖背后,Java 仍然将它们包装成函数式接口。因此,Java 将 lambda 表达式视为对象,这是 Java 中真正的一等公民。
纯函数的定义强调纯函数应该只根据参数返回一个值,并且没有任何副作用。
除了方法的预期行为之外,副作用可以是任何东西。例如,副作用可以是更新本地或全局状态,或在返回值之前保存到数据库。 (有些人也将日志记录视为副作用。)
这听起来与 Java 中的所有最佳实践完全相反。
作为一种面向对象的语言,Java 推荐将封装作为核心编程实践。它鼓励隐藏对象的内部状态并仅公开访问和修改它的必要方法。因此,这些方法并不是严格意义上的纯函数。
当然,封装和其他面向对象的原则只是建议,在 Java 中并不具有约束力。
事实上,开发人员最近开始意识到定义不可变状态的价值和定义没有副作用的方法的价值。
假设我们想要找到我们刚刚排序的所有数字的总和:
Integer sum(List<Integer> numbers) {
return numbers.stream().collect(Collectors.summingInt(Integer::intValue));
}
此方法仅取决于它接收的参数,因此它是确定性的。此外,它不会产生任何副作用。
那么,让我们看看我们如何处理合法的副作用。例如,出于真正的原因,我们可能需要将结果保存在数据库中。函数式编程中有一些技术可以在保留纯函数的同时处理副作用。
我们将在后面的部分讨论其中的一些。
不变性是函数式编程的核心原则之一,它是指实体在实例化后无法修改的属性。
在函数式编程语言中,这是由语言级别的设计支持的。但是在 Java 中,我们必须自己决定创建不可变的数据结构。
请注意,Java 本身提供了几种内置的不可变类型,例如 String。这主要是出于安全原因,因为我们在类加载和基于散列的数据结构中大量使用字符串作为键。还有其他几种内置的不可变类型,
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。