Java 中的函数式编程(一)概念

2021-10-18 21:31:13 +08:00
 amy815

写在前面

从 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); };
}

函数式编程的特性
函数式编程有几个重要的特性:

  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

没有“副作用( 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 
663 次点击
所在节点    问与答
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/808632

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX