资讯专栏INFORMATION COLUMN

《java 8 实战》读书笔记 -第十三章 函数式的思考

Donne / 1906人阅读

摘要:当我们希望能界定这二者之间的区别时,我们将第一种称为纯粹的函数式编程,后者称为函数式编程。函数式编程我们的准则是,被称为函数式的函数或方法都只能修改本地变量。另一种观点支持引用透明的函数式编程,认为方法不应该有对外部可见的对象修改。

一、实现和维护系统 1.共享的可变数据

如果一个方法既不修改它内嵌类的状态,也不修改其他对象的状态,使用return返回所有的计算结果,那么我们称其为纯粹的或者无副作用的
副作用就是函数的效果已经超出了函数自身的范畴。下面是一些例子。

除了构造器内的初始化操作,对类中数据结构的任何修改,包括字段的赋值操作(一个典型的例子是setter方法)。

抛出一个异常。

进行输入/输出操作,比如向一个文件写数据。

从另一个角度来看“无副作用”的话,我们就应该考虑不可变对象。不可变对象是这样一种对象,它们一旦完成初始化就不会被任何方法修改状态。这意味着一旦一个不可变对象初始化完毕,它永远不会进入到一个无法预期的状态。你可以放心地共享它,无需保留任何副本,并且由于它们不会被修改,还是线程安全的。如果构成系统的各个组件都能遵守这一原则,该系统就能在完全无锁的情况下,使用多核的并发机制

2.声明式编程

如果你希望通过计算找出列表中最昂贵的事务,摒弃传统的命令式编程“如何做”的风格,采用如下这种“要做什么”风格的编程通常被称为声明式编程(利用了函数库,内部迭代)。

Optional mostExpensive = 
 transactions.stream() 
 .max(comparing(Transaction::getValue));
3.为什么要采用函数式编程

使用函数式编程,你可以实现更加健壮的程序,还不会有任何的副作用。

二、什么是函数式编程

对于“什么是函数式编程”这一问题最简化的回答是“它是一种使用函数进行编程的方式”。

当谈论“函数式”时,我们想说的其实是“像数学函数那样——没有副作用”。由此,编程上的一些精妙问题随之而来。我们的意思是,每个函数都只能使用函数和像if-then-else这样的数学思想来构建吗?或者,我们也允许函数内部执行一些非函数式的操作,只要这些操作的结果不会暴露给系统中的其他部分?换句话说,如果程序有一定的副作用,不过该副作用不会为其他的调用者感知,是否我们能假设这种副作用不存在呢?调用者不需要知道,或者完全不在意这些副作用,因为这对它完全没有影响。当我们希望能界定这二者之间的区别时,我们将第一种称为纯粹的函数式编程,后者称为函数式编程

1.函数式 Java 编程

我们的准则是,被称为“函数式”的函数或方法都只能修改本地变量。除此之外,它引用的对象都应该是不可修改的对象。通过这种规定,我们期望所有的字段都为final类型,所有的引用类型字段都指向不可变对象。

要被称为函数式,函数或者方法不应该抛出任何异常。
那么,如果不使用异常,你该如何对除法这样的函数进行建模呢?答案是请使用Optional类型

最后,作为函数式的程序,你的函数或方法调用的库函数如果有副作用,你必须设法隐藏它们的非函数式行为,否则就不能调用这些方法。

2.引用透明性

如果一个函数只要传递同样的参数值,总是返回同样的结果,那这个函数就是引用透明的。

Java语言中,关于引用透明性还有一个比较复杂的问题。假设你对一个返回列表的方法调用了两次。这两次调用会返回内存中的两个不同列表,不过它们包含了相同的元素。如果这些列表被当作可变的对象值(因此是不相同的),那么该方法就不是引用透明的。如果你计划将这些列表作为单纯的值(不可修改),那么把这些值看成相同的是合理的,这种情况下该方法是引用透明的。通常情况下,在函数式编程中,你应该选择使用引用透明的函数

3.面向对象的编程和函数式编程的对比

作为Java程序员,毫无疑问,你一定使用过某种函数式编程,也一定使用过某些我们称为极端面向对象的编程。一种支持极端的面向对象:任何事物都是对象,程序要么通过更新字段完成操作,要么调用对与它相关的对象进行更新的方法。另一种观点支持引用透明的函数式编程,认为方法不应该有(对外部可见的)对象修改。

三、递归和迭代

纯粹的函数式编程语言通常不包含像while或者for这样的迭代构造器。之后你该如何编写程序呢?比较理论的答案是每个程序都能使用无需修改的递归重写,通过这种方式避免使用迭代。使用递归,你可以消除每步都需更新的迭代变量。
比如阶乘

static long factorialStreams(long n){ 
 return LongStream.rangeClosed(1, n) 
 .reduce(1, (long a, long b) -> a * b); 
}

每次执行factorialRecursive方法调用都会在调用栈上创建一个新的栈帧,用于保存每个方法调用的状态(即它需要进行的乘
法运算),这个操作会一直指导程序运行直到结束。这意味着你的递归迭代方法会依据它接收的输入成比例地消耗内存。这也是为什么如果你使用一个大型输入执行factorialRecursive方法,很容易遭遇StackOverflowError异常:

Exception in thread "main" java.lang.StackOverflowError

函数式语言提供了一种方法解决这一问题:尾调优化(tail-call optimization),基本的思想是你可以编写阶乘的一个迭代定义,不过迭代调用发生在函数的最后(所以我们说调用发生在尾部)。
基于“尾递”的阶乘

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

使用栈桢方式的阶乘的递归定义:

阶乘的尾递定义这里它只使用了一个栈帧:

坏消息是,目前Java还不支持这种优化。很多的现代JVM语言,比如Scala和Groovy都已经支持对这种形式的递归的优化,最终实现的效果和迭代不相上下(它们的运行速度几乎是相同的)。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74465.html

相关文章

  • Java8实战》-读书笔记第一章(02)

    摘要:实战读书笔记第一章从方法传递到接着上次的,继续来了解一下,如果继续简化代码。去掉并且生成的数字是万,所消耗的时间循序流并行流至于为什么有时候并行流效率比循序流还低,这个以后的文章会解释。 《Java8实战》-读书笔记第一章(02) 从方法传递到Lambda 接着上次的Predicate,继续来了解一下,如果继续简化代码。 把方法作为值来传递虽然很有用,但是要是有很多类似与isHeavy...

    lushan 评论0 收藏0
  • Java 8 函数式编程」读书笔记——lambda表达式

    摘要:本文是函数式编程第二章的读书笔记。的语法简化了使用匿名内部类时的模板代码,让程序员专注于编写想要执行的行为,也让代码更加简洁易读。中最重要的函数接口类型推断为新成员表达式提供了类型推断的支持,在不需要声明参数类型的表达式中表现的有为明显。 本文是「Java 8 函数式编程」第二章的读书笔记。 Lambda引入的变化 Lambda表达式,是一种紧凑的、传递行为的方式,从编程思想上来讲,...

    lx1036 评论0 收藏0
  • java 8 实战读书笔记 -第七章 并行数据处理与性能

    摘要:正确使用并行流错用并行流而产生错误的首要原因,就是使用的算法改变了某些共享状态。高效使用并行流留意装箱有些操作本身在并行流上的性能就比顺序流差还要考虑流的操作流水线的总计算成本。 一、并行流 1.将顺序流转换为并行流 对顺序流调用parallel方法: public static long parallelSum(long n) { return Stream.iterate(1L...

    刘福 评论0 收藏0
  • java 8 实战读书笔记 -第四章 引入流

    摘要:第四章引入流一什么是流流是的新成员,它允许你以声明性方式处理数据集合通过查询语句来表达,而不是临时编写一个实现。 第四章 引入流 一、什么是流 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码。 下面两段代码都是用来返回低...

    jeyhan 评论0 收藏0
  • Java 8 函数式编程」读书笔记——数据并行化

    摘要:限制编写并行流,存在一些与非并行流不一样的约定。底层框架并行流在底层沿用的框架,递归式的分解问题,然后每段并行执行,最终由合并结果,返回最后的值。 本书第六章的读书笔记,也是我这个系列的最后一篇读书笔记。后面7、8、9章分别讲的测试、调试与重构、设计和架构的原则以及使用Lambda表达式编写并发程序,因为笔记不好整理,就不写了,感兴趣的同学自己买书来看吧。 并行化流操作 关于并行与并发...

    leone 评论0 收藏0
  • java 8 实战读书笔记 -第九章 默认方法

    摘要:类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。只有声明了一个默认方法。由于比更加具体,所以编译器会选择中声明的默认方法。 如果在现存的接口上引入了非常多的新方法,所有的实现类都必须进行改造,实现新方法,为了解决这个问题,Java 8为了解决这一问题引入了一种新的机制。Java 8中的接口现在支持在声明方法的同时提供实现,这听起来让人惊讶!通过两种方式可以完成这种操作。其一...

    phoenixsky 评论0 收藏0

发表评论

0条评论

Donne

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<