Java函数式编程总结

Java函数式编程总结

在阅读SpringBoot的源码时,框架运用了大量的函数式编程。可以说,业务代码你可以不使用函数式编程,但是上升到框架层面,函数式编程是基础。

什么是函数式编程

函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算(lambda calculus)为该语言最重要的基础。而且,λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。

比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程

以上内容摘抄自维基百科,建议学习函数式编程前,先试着理解它的定义。当然,如果你十分聪明能够通过定义掌握函数式编程的思想,可以忽略以下的内容。如果你不能通过定义掌握它的思想,建议大家先循序渐进,敲敲代码,实战以下,再回来读这个定义,就会柳暗花明。

请大家细细品味斜体加粗的部分。

函数式编程特点

函数是“第一等公民”

所谓“第一等公民”(first class),指的是函数与其他数据类型一样,出于平等地位,可以赋值给其他变量,也可以作为参数传入或者返回。

在Java中,这种操作太常见了,比如SpringBoot源码中。入参是一个BiConsumer的函数,出参是一个自定义的函数DocumentConsumer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
if (checkForExisting) {
for (MutablePropertySources merged : this.loaded.values()) {
if (merged.contains(document.getPropertySource().getName())) {
return;
}
}
}
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
addMethod.accept(merged, document.getPropertySource());
};
}

@FunctionalInterface
private interface DocumentConsumer {
void accept(Profile profile, Document document);
}

只用“表达式”,不用“语句”

表达式(expression)是一个单纯的运算过程,总是有返回值。语句(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。换句话说,每一步都是单纯的运算,而且都有返回值。先说结论,在一个应用中,不可能全篇都是函数式编程,因为函数式不适合有I/O的场景,尽量把函数式应用在单纯的计算中。如果不能理解可以参考下下面对语句和表达式的说明。

语句

语句(statement)又称述句、陈述式、描述式、语句、陈述句等。在计算机科学的编程中,一个语句是指令式编程语言中最小的独立元素,表达程序要运行的一些动作。多数语句是以高级语言编写成一或多个语句的序列,用于命令计算机运行指定的一系列操作。单一个语句本身也具有内部结构(例如表达式)。

许多语言(例如说,C语言)将语句与定义句(definition)分隔的很明确,因为语句只会有运算符号以及一些宣告标识符号(identifier)的定义。我们也可以找出简单语句与复合语句之间的差异;后者会在一个段落中包含了许多语句。

计算机语句示例

表达式

在讲表达式的概念以前,我们需要先知道什么是“副作用”。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并降低程序的可读性。严格的函数式语言要求函数必须无副作用。例如修改全局变量(函数外的变量),修改参数或改变外部存储。

在大多数编程语言中,语句与表达式互相对比,两者不同之处在于,语句是为了运作它们的副作用而运行;表达式则一定会传回评估后的结果,而且通常不产生副作用

在措辞中经常出现这样的区别:一个语句是被“运行”(execute),而一个表达式是被“评估”或对其“求值”(evaluate)。一些语言中具备了exec和eval函数:比如在Python中,exec应用于语句,而eval应用于表达式。

##代码简洁,接近自然语言,易于理解

(1+2)*3-4

1
add(1,2).multiply(3).subtract(4)

##易于并发编程

函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。

但是仅仅靠函数式编程是离不开并发编程基础的,换句话说,你想利用多核,还是需要编写多线程代码,只是在Runnable接口的实现中,采用一段函数式代码,或者一个函数式模块。真正解决用串行思想异步多核编程,请参见Reactor

##更方便的代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

常用的函数式接口

在Java中,内置了很多函数式接口,它们大多数在java.util.function包中。

最常用的Consumer,Function,Supplier,Predicate。除了这些常用的之外,还有BiConsumer,BiFunction,BiPredicate等等,可以看他们源码中的注释来查看区别。

Consumer

接受一个参数,不返回参数,通俗讲它就是“消费者”。方法consumer的第一个入参是一个Map迭代器Consumer函数,第二个参数是要迭代的Map。Main方法中,通过Lambda表达式,构造了一个函数传递给consumer方法进行传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
map.put("d", 4);
map.put("e", 5);
consumer((x) -> x.forEach((key, value) -> System.out.println("key=" + key + ",value=" + value)), map);
}

private void consumer(Consumer<Map<String, Object>> iterator, Map<String, Object> map) {
iterator.accept(map);
}

#输出
key=a,value=1
key=b,value=2
key=c,value=3
key=d,value=4
key=e,value=5

Function

Function函数,接受一个参数T,返回值R。方法function第一个参数是一个类型转换器(Function函数),第二个参数是要转换的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
System.out.println(function((str) -> Integer.parseInt(str), "1024"));
/**
* 类型转换
*
* @param convert 传入一个输入为字符串,输出为整型的函数
* @param str 要转换的字符串
* @return 整数
*/
private Integer function(Function<String, Integer> convert, String str) {
return convert.apply(str);
}

#输出
1024

Supplier

Supplier函数,不需要输入,返回一个值,通俗讲它是一个“生产者“,而且是不需要“原料”的无私奉献者。

1
2
3
4
5
6
System.out.println(supplier(() -> 1));
private Integer supplier(Supplier<Integer> supplier) {
return supplier.get();
}
##输出
1

Predicate

Predicate函数,输入一个值,返回一个Boolean类型,通俗讲是一个预测函数,判定值是真是假。

1
2
3
4
5
6
7
8
9
10
11
12
13
System.out.println(predicate((x) -> x > 10, 11));
/**
* 判定输入数字是否大于10
* @param predicate 预测函数
* @param num 输入数字
* @return true or false
*/
private boolean predicate(Predicate<Integer> predicate, Integer num) {
return predicate.test(num);
}

##输出
true

自定义函数式接口

当然有很多情况Java自带的函数式接口不能满足我们的需求,我们也可以通过@FunctionInterface注解自定义函数式接口。

例如,上一个章节中讲解的四大常用函数,我们也可以自定义一套和他们功能一样的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FunctionalInterface
private interface MyConsumer {
void iterator(Map<String, Object> map);
}

@FunctionalInterface
private interface MyFunction {
Integer parse(String str);
}

@FunctionalInterface
private interface MySupplier {
Integer get();
}

@FunctionalInterface
private interface MyPredicate {
boolean test(Integer num);

default boolean negate(Integer num) {
return !test(num);
}
}

总结

函数式编程不是新生事物,在学术界已经存在很久。但是最近几年,在工业界开始兴起,它是一种编程范式和新的思想,和它平行的几个思想分别是面向过程编程面向对象编程反应式编程,各有千秋,各有各的应用场景,不要神化谁。

在Java中,从JDK8开始支持函数式编程,从实际工作中,即使不学习函数式编程,也基本不影响正常工作(除非团队硬性要求使用),而且我个人的经历,很多人比较排斥函数式编程,理由是“晦涩难懂,可维护性差,需要学习成本”等等。但是最近学习Spring Cloud和Spring5的源码,发现框架中大量使用函数式编程,可以说不接纳函数式编程的思想,就会被业界淘汰,而且最近刚刚兴起的反应式编程,也是以函数式编程为基础的。习惯面向过程编程的人,会说“面向对象语言真麻烦,这么多设计模式真讨厌,一个接口那么多实现类真烦”,函数式编程又何尝不是在各种质疑声中成为现代框架和基础组件中的“水电煤”。

参考

1、wiki百科函数式编程)

2、wiki百科语句(程序设计)](https://zh.wikipedia.org/zh-cn/語句_(程式設計))

3、Java函数式编程初始篇

评论