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

1、实现和维护系统

1.共享的可变数据

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

  • 除了构造器内的初始化操做,对类中数据结构的任何修改,包括字段的赋值操做(一个典型的例子是setter方法)。
  • 抛出一个异常。
  • 进行输入/输出操做,好比向一个文件写数据。

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

2.声明式编程

若是你但愿经过计算找出列表中最昂贵的事务,摒弃传统的命令式编程“如何作”的风格,采用以下这种“要作什么”风格的编程一般被称为声明式编程(利用了函数库,内部迭代)。编程

Optional<Transaction> mostExpensive = 
 transactions.stream() 
 .max(comparing(Transaction::getValue));

3.为何要采用函数式编程

使用函数式编程,你能够实现更加健壮的程序,还不会有任何的反作用。安全

2、什么是函数式编程

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

当谈论“函数式”时,咱们想说的实际上是“像数学函数那样——没有反作用”。由此,编程上的一些精妙问题随之而来。咱们的意思是,每一个函数都只能使用函数和像if-then-else这样的数学思想来构建吗?或者,咱们也容许函数内部执行一些非函数式的操做,只要这些操做的结果不会暴露给系统中的其余部分?换句话说,若是程序有必定的反作用,不过该反作用不会为其余的调用者感知,是否咱们能假设这种反作用不存在呢?调用者不须要知道,或者彻底不在乎这些反作用,由于这对它彻底没有影响。当咱们但愿能界定这两者之间的区别时,咱们将第一种称为纯粹的函数式编程,后者称为函数式编程并发

1.函数式 Java 编程

咱们的准则是,被称为“函数式”的函数或方法都只能修改本地变量。除此以外,它引用的对象都应该是不可修改的对象。经过这种规定,咱们指望全部的字段都为final类型,全部的引用类型字段都指向不可变对象。编程语言

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

最后,做为函数式的程序,你的函数或方法调用的库函数若是有反作用,你必须设法隐藏它们的非函数式行为,不然就不能调用这些方法。函数式编程

2.引用透明性

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

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

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

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

3、递归和迭代

纯粹的函数式编程语言一般不包含像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都已经支持对这种形式的递归的优化,最终实现的效果和迭代不相上下(它们的运行速度几乎是相同的)。

相关文章
相关标签/搜索