写在前面
从Java 8开始,Java语言添加了lambda表达式以及函数式接口等新特性。这意味着Java语言也开始逐步提供函数式编程的能力。
事实上,如果你熟悉Erlang、Scala、JavaScript或Python,那你或多或少对函数式编程相对熟悉。但如果你是一个通过常规路径学习的Javaer,可能对函数式编程思想不甚了解,相对的,你可能对面向对象编程思想会更熟悉。
先熟悉一下几个术语,有利于提升大家的逼格:
FP,Functional Programming,函数式编程;
OOP,Object Oriented Programming,面向对象编程;
虽然FP是在最近10年才流行起来的,但它的历史和OOP几乎等长。为什么FP突然流行起来了呢?
- 最主要原因是摩尔定律正在逐渐失效,单核CPU的计算能力在短期内无法有大的突破,计算机领域正在向“多核CPU和分布式计算”的方向发展。而FP则天然具备适合并发编程的优点:它不修改变量,不存在多线程间的竞争问题,因此也不需要考虑“锁”和“线程阻塞”,易于并发编程。在未来的大数据时代,函数式编程思想将会越来越重要。
- 其次,在现实的编码过程中,程序员们发现了即使在OOP的世界中,FP能更好的解决某些具体问题,是OOP的有益补充。因此,出现了许多混合式风格的代码(混合式指 OOP + FP ),这也是为什么Java这个老牌的OOP编程语言要引入FP新特性的原因所在。
本系列文章的重点在于介绍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 reducedef compose(*funcs) -> int:\"\"\"将一组简单函数 [f, g, h] 组合为一个复合函数 (f(g(h(...)))) \"\"\"return reduce(lambda f, g: lambda x: f(g(x)), funcs)# 例子f = lambda x: x + 1g = lambda x: x * 2h = lambda x: x - 3# 调用复合函数 f(g(h(x)):[(x-3) * 2] + 1print(compose(f, g, h)(10)) // print 15
在Java中,java.util.Objects.Consumer 接口的默认方法andThen是一个简单的函数组合函数:
default Consumer<T> andThen(Consumer<? super T> after) {Objects.requireNonNull(after);return (T t) -> { accept(t); after.accept(t); };}
函数式编程的特性
1. 函数是“第一等公民”(first-class citizens)
函数是“第一等公民”,意味着函数和其它数据类型具备同等的地位——可以赋值给某个变量,可以作为另一个函数的参数,也可以作为另一个函数的返回值。
判断某种开发语言对函数式编程支持程度高低,一个重要的标准就是该语言是否把函数作为“第一等公民”。
例如下面的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
2. 没有“副作用(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 {return reverse(arg.substring(1)) + arg.substring(0, 1);}}
我们可以使用递归来解决类似的问题,虽然它的性能实在不敢恭维,但它确实完全符合函数式编程风格。此时,不免有一个疑问:函数式编程限制如此的多,性能看起来也不太好,我们为什么还要提倡函数式编程呢?原因就在于函数式编程有着许多的优点,尤其是在大数据 && 多核计算的时代。
3. 引用透明(Referential transparency)
引用透明(Referential transparency),指的是函数的运行不依赖于外部状态或外部变量,只依赖于输入的参数。任何时候只要参数相同,运行函数所得到的返回值总是相同的。
在其他的编程范式中,函数的返回值往往与系统状态有关。不同的状态之下,返回值可能不一样。这很不利于对程序进行测试和调试。
函数式编程的优点
1. 便于单元测试和调试(Debugging)
由于函数式编程的“没有副作用”和“引用透明”的特点,每个函数都是一个独立的逻辑单元,不依赖于外部的状态或变量,也不修改外部的状态或变量。给定了参数后,多次运行函数,总会得到相同的返回值。这对单元测试者以及调试者来说,简直是最理想的情形。
2. 易于“并发编程”
同样的,函数式编程不依赖于也不会修改外部的状态或变量,因此我们不需要考虑多线程的并发竞争、死锁等问题,因为我们根本不需要加“锁”。这样,并发编程的复杂度将大大降低,部署也非常方便。
在大数据和多核时代,这个优点被更大的放大了,这也是函数式编程思想焕发活力的主要原因。
3. 便于热部署
这个优点也很显然——因为函数式编程不依赖于也不会修改外部的状态或变量,所以只要保持接口不变,我们可以随时升级代码,不需要重启服务。
结语
需要谨记,函数式编程(FP)是一种编程范式或者说是一种编程思想,和它可以类比的是面向对象编程(OOP)。
OOP的精髓在于“封装对象”,而FP的精髓在于“不涉及外部状态”。
一门开发语言,可以既支持OOP,同时也支持FP,两者并不是矛盾的。
一门开发语言,即使它号称真正支持函数式编程,也不可能100%符合函数式编程风格,因为IO操作是有“副作用”的,但实际上,不做I/O是不可能的。因此,在实际应用中,函数式编程只要求把I/O的影响限制到最小,并更专注于保持计算过程的单纯性。