《Java8实战》读书笔记12:函数式编程
Posted 笑虾
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java8实战》读书笔记12:函数式编程相关的知识,希望对你有一定的参考价值。
《Java8实战》读书笔记12:函数式编程
第 13 章 函数式的思考
本章内容
为什么要进行函数式编程
什么是函数式编程
声明式编程以及引用透明性
编写函数式Java的准则
迭代和递归
13.1 实现和维护系统
- 接二手项目时,修复并发导致的缺陷是很困难的。
- 如果你喜欢无状态的行为
Java 8
中新增的Stream
提供了强大的技术支撑,让我们无需担心锁引起的各种问题,充分发掘系统的并发能力。
13.1.1 共享的可变数据
一个数据被多方修改是一切并发问题的起因,是万恶之源。
如果一个方法既不修改它内嵌类的状态,也不修改其他对象的状态,使用return返回所有的计算结果,那么我们称其为纯粹的或者无副作用的。
不可变对象:它们一旦完成初始化就不会被任何方法修改状态。它是线程安全的。
如果构成系统的各个组件都能做到“无副作用”,该系统就能在完全无锁的情况下,使用多核的并发机制,因为任何一个方法都不会对其他的方法造成干扰。
13.1.2 声明式编程
命令式编程:适合传统的面向对象。因为它的特点是它的指令和计算机底层的词汇非常相近,比如赋值、条件分支以及循环。
声明式编程:函数式编程
是声明式编程
的一种实现。代码更倾向于描述业务
,陈述问题
。
13.2 什么是函数式编程
函数式编程:是一种使用函数进行编程的方式。
在函数式编程的上下文中,一个“函数”对应于一个数学函数:它接受零个或多个参数,生成一个或多个结果,并且不会有任何副作用。你可以把它看成一个黑盒,它接收输入并产生一些输出,如图13-3所示。
纯粹的函数式编程:使用没有任何副作用的函数。
函数式编程:
- 调用者不知道,或者完全不在意它的修改。
- 只有 return 一条出跟。
- 因此也不应该向函数外抛异常。
Optional<T>
前来报道。
13.2.1 函数式 Java 编程
编程实战中,你是无法用Java语言以纯粹的函数式来完成一个程序的。
13.2.2 引用透明性
“没有可感知的副作用”(不改变对调用者可见的变量、不进行I/O、不抛出异常)的这些限
制都隐含着引用透明性。如果一个函数只要传递同样的参数值,总是返回同样的结果,那这个函数就是引用透明的。
缓存机制也可以理解为一种引用透明的。
通常情况下,在函数式编程中,你应该选择使用引用透明的函数。
13.2.3 面向对象的编程和函数式编程的对比
极端的函数式编程
和面向对象编程
都没有意义,我们的目的不是搞宗教。对丰富我们的编程武器库。
13.2.4 函数式编程实战
让我们从解决一个示例函数式的编程练习题入手:给定一个列表List,比如1, 4, 9,构造一个List<List>,它的成员都是类表1, 4, 9的子集——我们暂时不考虑元素的顺序。1, 4, 9的子集是1, 4, 9、1, 4、1, 9、4, 9、1、4、9以及。
以下是书中实现代码。insertAll
和concat
都没有修改入参
,函数外变量
,它们是函数式
的,因为使用它们的subsets
在自己没犯错的情况也,就也是函数式
的。
static List<List<Integer>> insertAll(Integer first, List<List<Integer>> lists)
List<List<Integer>> result = new ArrayList<>(); // 新建返回值对象
for (List<Integer> list : lists)
List<Integer> copyList = new ArrayList<>();
copyList.add(first);
copyList.addAll(list);
result.add(copyList);
return result;
static List<List<Integer>> concat(List<List<Integer>> a, List<List<Integer>> b)
List<List<Integer>> r = new ArrayList<>(a); // 新建返回值对象
r.addAll(b);
return r;
static List<List<Integer>> subsets(List<Integer> list)
if (list.isEmpty())
List<List<Integer>> ans = new ArrayList<>(); // 新建返回值对象
ans.add(Collections.emptyList());
return ans;
Integer first = list.get(0);
List<Integer> rest = list.subList(1,list.size());
List<List<Integer>> subans = subsets(rest);
List<List<Integer>> subans2 = insertAll(first, subans);
return concat(subans, subans2);
@Test
public void testSubsets()
List<Integer> list = Arrays.asList(1, 4, 9);
List<List<Integer>> subsets = subsets(list);
System.out.println(JSON.toJSONString(subsets));
13.3 递归和迭代
函数式
推崇递归
不好迭代
,并提出理论上所有迭代
都可以用递归
实现。但也不避讳,Java中递归开销更大(难道有种语言不是?可能专门为函数式设计的语言可以吧)
代码清单13-1 迭代式的阶乘计算
static int factorialIterative(int n)
int r = 1;
for (int i = 1; i <= n; i++)
r *= i;
return r;
代码清单13-2 递归式的阶乘计算
static long factorialRecursive(long n)
return n == 1 ? 1 : n * factorialRecursive(n-1);
代码清单13-3 基于Stream的阶乘
static long factorialStreams(long n)
return LongStream.rangeClosed(1, n)
.reduce(1, (long a, long b) -> a * b);
代码清单13-4 基于“尾-递”的阶乘
static long factorialTailRecursive(long n)
return factorialHelper(1, n);
static long factorialHelper(long acc, long n)
return n == 1 ? acc : factorialHelper(acc * n, n-1);
图13-5和图13-6解释了使用递归和“尾递”实现阶乘定义的不同。
13.4 小结
- 下面是这一章中你应该掌握的关键概念。
- 从长远看,
减少共享的可变数据
结构能帮助你降低维护和调试程序的代价。 函数式编程
支持无副作用的方法
和声明式编程
。函数式方法
可以由它的输入参
数及输出结果
进行判断。- 如果一个函数使用相同的参数值调用,总是返回相同的结果,那么它是引用透明的。采用递归可以取得迭代式的结构,比如while循环。
- 相对于Java语言中传统的递归,“尾-递”可能是一种更好的方式,它开启了一扇门,让我们有机会最终使用编译器进行优化。(坏消息是,目前Java还不支持这种优化。很多的现代JVM语言已经支持。最终实现的效果和迭代不相上下。)
第 14 章 函数式编程的技巧
本章内容
一等成员、高阶方法、科里化以及局部应用
持久化数据结构
生成Java Stream时的延迟计算和延迟列表
模式匹配以及如何在Java中应用
引用透明性和缓存
14.1 无处不在的函数
14.1.1 高阶函数(higher-order function)
- 接受至少一个函数作为参数
- 返回的结果是一个函数
副作用和高阶函数
将所有你愿意接收的作为参数的函数可能
带来的副作用
以文档
的方式记录
下来是一个不错的设计原则,最理想的情况
下你接收的函数参数应该没有任何副作用
!
14.1.2 科里化
科里化的理论定义
科里化①是一种将具备2个参数(比如,x和y)
的函数f
转化为使用一个参数的函数g
,并且这个函数的返回值也是一个函数,它会作为新函数的一个参数。后者的返回值和初始函数的返回值相同,即f(x,y) = (g(x))(y)
。
当然,我们可以由此推出:你可以将一个使用了6个参数的函数科里化成一个接受第2、4、 6号参数,并返回一个接受5号参数的函数,这个函数又返回一个接受剩下的第1号和第3号参数的函数。
一个函数使用所有参数仅有部分被传递时,通常我们说这个函数是部分应用的(partially applied)。
文字太绕,上代码,用javascript
演示就更直观了:
原本的 f
接收 x, y
两个参数。
function f(x, y)
return x + y;
科里化后的 fCurry
接收 y
返回一个 g
, g
接收一个参数 x
。写出来一看,这不就是闭包嘛。
function fCurry(y)
return function (x)
return x + y;
var g = fCurry(3);
var result = g(5);
console.log(result); // 8
换成 Lambda
原地飞升,有木有?
var fCurry = (y) => (x) => x+y;
var g = fCurry(3);
console.log(g(1)); // 4
console.log(g(2)); // 5
console.log(g(3)); // 6
14.2 持久化数据结构
本节的核心思想就是再次强调:
- 不要修改入参。
- 有变化的部分,要创建副本来作为返回结果,但可以引用原有数据结构的部分。(禁止对现存数据结构的修改,因为所有人都不允许修改这些数据,所以不用担心它出问题)
- 外部数据就更不能动了。(每个人都应该只动自己的东西)
- 理论上你应该把自己的字段都声明为
final
。
14.3 Stream 的延迟计算
Java 8
的Stream
以其延迟性而著称,借助延迟计算可以按需劳动。比一下把所有要用的对象都创建出来(面且有些情况下,这些对象还没上阵杀敌,就被销毁了。非常浪费),性能更好。
但延时计算也有额外开销,并非100%就是最优方案。无用功做太多了也不行,归根结底就是一个空间与时间互换的问题。得根据业务需要具体分析。
- 简单举例:
原本 getter
public Hero getHero()
return hero; // 这个 英雄对象老早就创建好占着内存了。
改成延时计算的。传统的传递工厂类也能实现,但现在有 函数式接口
了,可以直接传 lambda
了。
public Hero getHero()
return () ->
return new Hero("随机姓名", 随机等级); // 这个只有到get时,才创建。
;
14.4 模式匹配
Java8 中暂时并未提供这一特性,本节我们先看下猪跑,为以后吃猪肉做点心里准备。
个人理解不确定对不对:对象如果想实现 +-*/
等操作,只能通过方法调用。但是又觉得麻烦,说是通过封装简化,把:操作符
、操作数
装到一个 操作对象
中,然后执行这个操作对象得到结果。但这也很复杂,于是就有了这个模式匹配
,简化代码,实现。
由于 Java8
中尚无此特性,所以只是模拟实现:
Expr e = new BinOp("+", new Number(5), new Number(0)); // 封装操作对象
Expr match = simplify(e); // 使用简化的操作函数 simplify 执行操作,得到结果。
System.out.println(match); // 输出结果
总之很绕。。。
14.5 杂项
14.5.1 缓存或记忆表
final Map<Range,Integer> numberOfNodes = new HashMap<>();
Integer computeNumberOfNodesUsingCache(Range range)
Integer result = numberOfNodes.get(range);
if (result != null)
return result;
result = computeNumberOfNodes(range);
numberOfNodes.put(range, result);
return result;
Java 8
改进了 Map
接口,提供了一组名为 computeIfAbsent
的方法,表示 如果存在
则执行
(还有一个相反的 computeIfPresent
,在附录B会一起介绍)
Integer computeNumberOfNodesUsingCache(Range range)
return numberOfNodes.computeIfAbsent(range, this::computeNumberOfNodes);
最后还是那句:如果你以函数式的方式进行程序设计,那就完全不必担心你的方法是否使用了正确的同步方式,因为你清楚地知道它没有任何共享的可变状态。
14.5.2 “返回同样的对象”意味着什么
关于是否符合引用透明性原则:函数式编程通常不使用 ==
(引用相等),而是使用 equal
对数据结构值进行比较,由于数据没有发生变更,所以 14.2
中的 fupdate
方法是引用透明的。
14.5.3 结合器
函数式编程
时编写高阶函数
是非常普通而且非常自然的事。高阶函数接受两个或多个函数
,并返回另一个函数
,实现的效果在某种程度上类似于将这些函数进行了结合
。术语结合器
通常用于描述这一思想。Java 8中的很多API都受益于这一思想,比如CompletableFuture
类中的thenCombine
方法。该方法接受两个CompletableFuture
方法和一个BiFunction
方法,返回另一个CompletableFuture
方法。
(我理解他这说“两个CompletableFuture
”的意思是指对象自己和thenCombine
的第一个参数 )
// 第一个 future
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 1 + 2);
// 第二个 future
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 2 * 2);
// f1 和 f2 得到结果后再用这个 biFunction 处理
BiFunction<Integer, Integer, Integer> biFunction = (a, b) -> a * b;
// 通过 thenCombine 把 f1、f2 结合起来。
CompletableFuture<Integer> cfCombine = f1.thenCombine(f2, biFunction);
Integer integer = cfCombine.get();
System.out.println(integer); // 12
函数式简直是递归狂魔。。。
最后给了一个 compose
嵌套实现 repeat
功能
Function<Integer, Integer> f = (Integer x) -> 2 * x;
// repeat 返回了一个套三层的 f
Function<Integer, Integer> repeat = repeat(3, f);
Integer result = repeat.apply(10);
System.out.println(result); // 80
// 手动拼一下套三层的 f
Function<Integer, Integer> compose = compose(f, compose(f, f));
Integer result2 = compose.apply(10);
System.out.println(result2); // 80
14.6 小结
下面是本章中你应该掌握的重要概念。
一等函数
是可以作为参数
传递,可以作为返回结果
,同时还能存储在数据结构中
的函数。高阶函数
就是:入参是一个或者多个函数
,在对它们拼接组装后,又返回一个新函数
的函数。(能够“接收函数,加工函数,返回函数”的函数。)Java 中典型的高阶函数包括comparing、andThen、compose
。科里化
是一种帮助你模块化函数和重用代码的技术。(比如:js
中我们用闭包
实现的就是一个柯里化函数
)- 持久化数据结构在其被修改之前会对自身前一个版本的内容进行备份。因此,使用该技术能避免不必要的防御式复制。
- Java语言中的Stream不是自定义的。
延迟列表
是Java语言中让Stream
更具表现力的一个特性
。延迟列表让你可以通过
辅助方法(supplier
)即时地创建
列表中的元素
,辅助方法能帮忙创建更多的数据结构。- 模式匹配是一种函数式的特性,它能帮助你解包数据类型。它可以看成Java语言中switch语句的一种泛化。
- 遵守“
引用透明性
”原则的函数
,其计算结构可以
进行缓存
。结合器
是一种函数式
的思想
,它指的是将两个或多个函数
或者数据结构
进行合并
。
读书总结
这两章对函数式
推崇备至,其终极目的就是并发
:
- 往死里玩
递归
. 不可变数据结构
:所有数据结构最好都要不可变
。要修改
就创建副本来用,不要动原数据。无副作用函数
:只有所有人都不会动共享数据,才能实现引用透明
。(实现了引用透明
就可以肆无忌惮的玩并发
)高阶函数
:函数式编程
字面理解就是面向函数编程
,以前编程我们都是折腾数据
(对象)。现在变成了折腾函数
,只是换了个看问题的角度。科里化
:这是折腾函数
的一个具体方案。不可变数据结构
和无副作用函数
想要的效果都是消除多线程争抢资源
的情况。没有锁,没有阻塞,才能快乐的并行
。
以上是关于《Java8实战》读书笔记12:函数式编程的主要内容,如果未能解决你的问题,请参考以下文章
《Java8实战》读书笔记10:组合式异步编程 CompletableFuture
《Java8实战》读书笔记10:组合式异步编程 CompletableFuture