写在前面
从 Java 8 开始,Java 语言添加了 lambda 表达式以及函数式接口等新特性。这意味着 Java 语言也开始逐步提供函数式编程的能力。
事实上,如果你熟悉 Erlang 、Scala 、JavaScript 或 Python,那你或多或少对函数式编程相对熟悉。但如果你是一个通过常规路径学习的 Javaer,可能对函数式编程思想不甚了解,相对的,你可能对面向对象编程思想会更熟悉。
先熟悉一下几个术语,有利于提升大家的逼格:
FP,Functional Programming,函数式编程
OOP,Object Oriented Programming,面向对象编程
虽然 FP 是在最近 10 年才流行起来的,但它的历史和 OOP 几乎等长。
本系列文章的重点在于介绍 Java 中的函数式编程,Java 作为一个经典的 OOP 编程语言,在实际应用中,大部分 Java 程序都是 OOP+FP 的混合式代码。因此,对于函数式编程中的一些高级特性和技巧,例如 Currying 、惰性求值、尾递归等,我们不做专门的阐述,感兴趣的同学,可以搜索公众号,员说,查看完整文章,一起讨论。
下面,我们先了解一下函数式编程的定义以及它的优点。
本文的示例代码可从 gitee 上获取:
https://gitee.com/cnmemset/javafp
什么是函数式编程?
函数式编程是一种编程范式( programming paradigm ),追求的目标是整个程序都由函数调用( function applying )以及函数组合( function composing )构成的。
函数调用大家容易理解,但在函数式编程中,函数调用有一个限制——它不会改变函数以外的其它状态,换而言之,即函数调用不会改变在该函数之外定义的变量值。这种函数有个专门的术语——纯函数( purely function )。纯函数有个特点,当参数值不变的时候,多次运行纯函数,得到的结果总是一样的。这个特点特别有利于对纯函数进行 unit test 和 debugging 。
函数组合指的是将一系列简单函数组合起来形成一个复合函数。函数组合是一个相对复杂的概念,譬如在 Python 中:
from functools import reduce
def compose(*funcs) -> int:
"""将一组简单函数 [f, g, h] 组合为一个复合函数 (f(g(h(...)))) """
return reduce(lambda f, g: lambda x: f(g(x)), funcs)
# 例子
f = lambda x: x + 1
g = lambda x: x * 2
h = lambda x: x - 3
# 调用复合函数 f(g(h(x)):[(x-3) * 2] + 1
print(compose(f, g, h)(10)) // print 15
在 Java 中,java.util.Objects.Consumer<t style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> 接口的默认方法 andThen 是一个简单的函数组合函数:</t>
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
函数式编程的特性
函数式编程有几个重要的特性:
函数是“第一等公民”,意味着函数和其它数据类型具备同等的地位——可以赋值给某个变量,可以作为另一个函数的参数,也可以作为另一个函数的返回值。
判断某种开发语言对函数式编程支持程度高低,一个重要的标准就是该语言是否把函数作为“第一等公民”。
例如下面的 Java 代码,print 变量可以看做是一个匿名函数,它作为一个参数传入了函数 ArrayList.forEach 。更多的语言细节可以参考随后的系列文章。
public static void simpleFunctinoProgramming() {
List<String> l = Arrays.asList("a", "b", "c");
Consumer<String> print = s -> System.out.println(s);
l.forEach(print);
}
上述代码会输出:
a
b
c
没有“副作用( side effects )”
“副作用( side effects )”,指的是函数在执行的时候,除了得出计算结果之外,还会改变函数以外的状态。“副作用”的典型场景就是修改了程序的全局变量(譬如 Java 中某个全局可见的类的属性值、某个类的静态变量等等);修改传入的参数也属于“副作用”之一; IO 操作或调用其它有“副作用”的函数也属于“副作用”。
函数式编程中要求函数都是“纯函数( purely function )”。给定了参数后,多次运行纯函数,总会得到相同的返回值,而且它不会修改函数以外的状态或产生其它的“副作用”。
“副作用”的含义是如此苛刻,但有的时候我们需要在计算过程中保存状态,然而我们又不能使用可变量,此时我们使用递归,利用保存在栈上的参数来记录状态。下面的代码是一个经典的实例,它定义了一个将字符串反转的函数 reverse 。可以看到,reverse 在执行时的中间状态,是通过它在递归时的参数来保存的。务必牢记,在函数式编程中,所有的参数和变量都是 final 的(只能赋值 1 次而且赋值后不可变):
public static String reverse(final String arg) {
if (arg.length() == 0) {
return arg;
} else