Chapter 13. Thinking functionally

13.1 实现和维护系统

有synchronized关键字的不要维护

容易使用的程序

  • Stream的无状态的行为(函数不会由于需要等待从另一个方法中读取变量,或者由于需要写入的变量同时有另一个方法正在写入,而发生中断)让我们
  • 最好类的 结构应该反映出系统的结构
  • 提供指标对结构的合理性进行评估,比如耦合性(软件系统中各组件之间是否相互独立)以及内聚性(系统的各相关部分之间如何协作)

不过对于日常事务,最关心的是代码维护时的调试:代码遭遇一些无法预期的值就有可能发生崩溃。这些无法预知的变量都源于共享的数据结构被你所维护的代码中的多个方法读取和更新。

对此,函数式编程提出的“无副作用”以及“不变性”

无副作用

函数:如果一个方法既不修改它内嵌类的状态,也不修改其他对象的状态,使用return返回所有的计算结果,那么我们称其为无副作用的。函数如果抛出异常,I/O和对类中的数据进行任何修改(除构造器内的初始化)都是有副作用的。

  • 变量:final型

声明式编程

一般通过编程实现一个系统,有两种思考方式。一种专注于如何实现,另一种方式则更加关注要做什么。

前一种为经典的面向对象编程,命令式;后一种为内部迭代,声明式。

第二种方式编写的代码更加接近问题陈述。

函数式编程实现了上述的两种思想:使用不相互影响的表达式,描述想要做什么(由系统来选择如何实现)。

13.2 函数式编程

1.函数式Java编程

Java语言无法实现纯粹函数式(完全无副作用)的程序,只能接近(副作用不会被察觉)。

这种函数只能修改局部变量,它的引用对象(参数及其他外部引用)都是不可修改对象(复制后再使用非函数式行为,如add)。除此之外,不抛异常(用Optional,或者局部抛异常),不进行I/O。

2.引用透明性(上面规定的隐含)

一个函数只要传递同样的参数值,它总是返回同样(==)的结果。

3.例子

//给定一个List<value>,返回其子集,类型为List<List<Integer>>,下面是整体算法,下下面是函数式的实践
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); //
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<>();//复制新list,而不是直接用参数调用.add
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;
}

13.3 递归和迭代

将增强for改为迭代器方式没有副作用?

Iterator<Apple> it = apples.iterator();
while (it.hasNext()) {
Apple apple = it.next();
// ...
}

利用递归而非迭代来消除没步都需更新迭代变量(但是使用迭代在Java效率通常更差),如阶乘:

//下面代码除效率问题,还有StackOverflowError风险
static long factorialRecursive(long n) {
return n == 1 ? 1 : n * factorialRecursive(n-1);
} //尾迭代能解决StackOverflowError问题。每次调用函数时,把新的结果传入函数。遗憾Java目前还不支持这种优化,Scala可以。
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);
} //Stream更简单
static long factorialStreams(long n){
return LongStream.rangeClosed(1, n)
.reduce(1, (long a, long b) -> a * b);
}

总结:尽量使用Stream取代迭代操作,从而避免变化带来的影响。此外,如果递归能不带任何副作用地让你以更精炼的方式实现算法,你就应该用递归替换迭代,因为它更加易于阅读、实现和理解。大多数时候编程的效率要比细微的执行效率差异重要得多。

Chapter 14. Functional programming techniques

函数式语言更广泛的含义是:函数可以作参数、返回值,还能存储。

1.高阶函数

接受至少一个函数做参数,返回结果是一个函数

接收的作为参数的函数可能带来的副作用以文档的方式记录下来,最理想的情况下,接收的函数参数应该没有任何副作用。

2.科里化

一种将具备n个参数(比如,x和y)的函数f转化为使用m(m < n)个参数的函数g,并且这个函数的返回值也是一个函数,它会作为新函数的一个参数。后者的返回值和初始函数的 返回值相同。

//将下面函数科里化,即预设各种f和b的组合,需要使用时只需调用相应的函数加上确实的x。
static double converter(double x, double f, double b) {
return x * f + b;
} //创建高阶函数
static DoubleUnaryOperator curriedConverter(double f, double b){
return (double x) -> x * f + b;
}
//其中一种组合
DoubleUnaryOperator convertUSDtoGBP = curriedConverter(0.6, 0);
//使用
double gbp = convertUSDtoGBP.applyAsDouble(1000);

14.2 持久化数据结构

这里指的不是数据库中的持久化

链表例子(火车旅行)

//TrainJourney类(火车站)有两个公有变量,price和onward(下一站),构造函数如下
public TrainJourney(int p, TrainJourney t) {
price = p;
onward = t;
} //link方法把两个单向链表(一列火车站)连成一体。下面代码在a的基础上连接,这样会破坏原来a的结构。如果a原本在其他地方有应用,那么那些地方也会受到影响。
static TrainJourney link(TrainJourney a, TrainJourney b){
if (a==null) return b;
TrainJourney t = a;
while(t.onward != null){
t = t.onward;
}
t.onward = b;
return a;
} //函数式实现。下面的实现的结果是a的副本,后面接上b。所以要确保结果不被修改,否则b也会没修改。这也包括下面的tree例子
static TrainJourney append(TrainJourney a, TrainJourney b){
return a==null ? b : new TrainJourney(a.price, append(a.onward, b));
} //函数式难免会有一定程度的复制,上面例子至少只复制了a,而不是在一个全新的list上连接a和b

树例子(个人信息)

//节点信息,如果强制遵守函数式编程,可以将下面变量声明为final
class Tree {
private String key;
private int val;
private Tree left, right;
public Tree(String k, int v, Tree l, Tree r) {
key = k; val = v; left = l; right = r;
}
} //函数式的节点更新,每次更新都会创建一个新tree,通常而言,如果树的深度为d,并且保持一定的平衡性,那么这棵树的节点总数是2^d
public static Tree fupdate(String k, int newval, Tree t) {
return (t == null) ?
new Tree(k, newval, null, null) :
k.equals(t.key) ?
new Tree(k, newval, t.left, t.right) :
k.compareTo(t.key) < 0 ?
new Tree(t.key, t.val, fupdate(k,newval, t.left), t.right) :
new Tree(t.key, t.val, t.left, fupdate(k,newval, t.right));
}

?实现部分函数式(某些数据更新对某些用户可见):

  • 典型方式:只要你使用非函数式代码向树中添加某种形式的数据结构,请立刻创建它的一份副本
  • 函数式:改动前,复制修改处之前的部分,然后接上剩余部分

14.3 Stream 的延迟计算

有一个延迟列表的实现例子

如果延迟数据结构能让程序设计更简单,就尽量使用它们。如果它们会带来无法接受的性能损失,就尝试以更加传统的方式重新实现它们。

14.4 模式匹配(Java暂未提供)

1.访问者设计模式

一个式子简化的代码,如5+0变为5,使用Expr.simplify。但一开始要对expr进行各种检查,如expr的类型,不同类型有不同变量,当符合条件才返回结果。这个过程涉及instanceof和cast等操作,比较麻烦。

而访问者设计模式能得到一定的简化,它需要创建一个单独的类(SimplifyExprVisitor),这个类封装了一个算法(下面的visit),可以“访问”某种数据 类型。

class BinOp extends Expr{
String opname;
Expr left, right; public Expr accept(SimplifyExprVisitor v){
return v.visit(this);
}
} public class SimplifyExprVisitor {
...
public Expr visit(BinOp e){
if("+".equals(e.opname) && e.right instanceof Number && ...){
return e.left;
}
return e;
}
} //Java 中模式的判断标签被限制在了某些基础类型、枚举类型、封装基础类型的类以及String类型。
//Scala的简单实现
def simplifyExpression(expr: Expr): Expr = expr match {
case BinOp("+", e, Number(0)) => e
case BinOp("*", e, Number(1)) => e
case BinOp("/", e, Number(1)) => e
case _ => expr
}

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

这段代码虽然是透明的,但并不是线程安全的(numberOfNodes可变)

2.结合器

Chapter 15. comparing Java 8 and Scala

下面默认先写Scala,或只写Scala

15.1 Scala 简介

1.你好

//命令式
object Beer {//单例对象
def main(args: Array[String]/*Java先类型后变量*/){//不需要void,通常非递归方法不需要写返回类型;对象声明中的方法是静态的
var n : Int = 2
while( n <= 6 ){
println(s"Hello ${n} bottles of beer")
n += 1
}
}
} //函数式
2 to 6 /*Int的to方法,接受Int,返回区间,即也可以用2.to(6)。后面foreach理解相同*/foreach { n => println(s"Hello ${n} bottles of beer") }

同样一切为对象,但没有基本类型之分

Scala中用匿名函数或闭包指代lambda

2.数据结构

Map:

val authorsToAge = Map("Raoul" -> 23)Java需要创建后put

val authors = List("Raoul", "Mario")

Scala中的集合默认都是持久化的:更新一个Scala集合会生成一个新的集合,这个新的集合和之前版本的集合共享大部分的内容,最终的结果是数据尽可能地实现了持久化。由于这一属性,代码的隐式数据依赖更少:人们对代码中集合变更的困惑(比如在何处更新了集合,什么时候做的更新)也会更少。

val newNumbers = numbers + 8numbers为Set,添加元素是创建一个新Set对象

Java的不可变(immutable)比不可修改(unmodifiable)更彻底

val fileLines = Source.fromFile("data.txt").getLines.toList()
val linesLongUpper
= fileLines.filter(l => l.length() > 10)
.map(l => l.toUpperCase())
//另一种表达,多加.par表示并行
fileLines.par filter (_.length() > 10) map(_.toUpperCase())

元组

val book = (2014, "Java 8 in Action", "Manning")可不同类型,任意长度(上限23)

Java需要自己建pair类,�且3个以上元素的pair比较麻烦

Stream

Scala中可以访问之前计算的值,可以通过索引访问,同时内存效率会变低。

Option

和Java很像

def getCarInsuranceName(person: Option[Person], minAge: Int) =
person.filter(_.getAge() >= minAge)
.flatMap(_.getCar)
.flatMap(_.getInsurance)
.map(_.getName)
.getOrElse("Unknown")

15.2 函数

Scala多了“能够读写非本地变量”和对科里化的支持

1.一等函数

//filter的函数签名
def filter[T](p: (T) => Boolean/*Java用函数式接口Predicate<T>或 者Function<T, Boolean>,Scala直接用函数描述符或名为函数类型*/): List[T]//参数类型 //定义函数
def isShortTweet(tweet: String) : Boolean = tweet.length() < 20
//使用函数,tweets是List[String]
tweets.filter(isShortTweet).foreach(println)

2.匿名函数和闭包

//上面代码的匿名方式如下(都是语法糖)
val isLongTweet : String => Boolean
= (tweet : String) => tweet.length() > 60 isLongTweet("A very short tweet") //Java的匿名方式
Function<String, Boolean> isLongTweet = (String s) -> s.length() > 60; boolean long = isLongTweet.apply("A very short tweet"); //闭包
var count = 0
val inc = () => count+=1
inc()
println(count)
//Java
int count = 0;
Runnable inc = () -> count+=1;//会出错,count必须为final或效果为final
inc.run();

3.科里化

Java需要手工地切分函数,麻烦在于多参数情况

//Java
static Function<Integer, Integer> multiplyCurry(int x) {
return (Integer y) -> x * y;
} Stream.of(1, 3, 5, 7)
.map(multiplyCurry(2))
.forEach(System.out::println); //Scala
def multiplyCurry(x :Int)(y : Int) = x * y val multiplyByTwo : Int => Int = multiplyCurry(2)
val r = multiplyByTwo(10)

15.3 类和trait

1.类

Scala中的getter和setter都是隐式实现的

class Student(var name: String, var id: Int)

val s = new Student("Raoul", 1)
println(s.name)//getter
s.id = 1337//setter

2.trait

与interface类似,有抽象方法、默认方法、接口多继承。但trait还有抽象类的字段。Java支持行为的多继承,但还不支持对状态的多继承。

Scala可以在类实例化时才决定trait

val b1 = new Box() with Sized

Chapter 16. Conclusions

Java8的发展体现了两种趋势:多核处理的需求(独立CPU速度瓶颈)->并行计算;更简洁地对抽象数据进行操作。

1.行为参数化:Lambda和方法引用

2.对大量数据的处理Stream:能在一次遍历中完成多种操作,而且按需计算。

并行处理中的重点:无副作用、Lambda、方法引用、内部迭代

3.CompletableFuture提供了像thenCompose、thenCombine、allOf这样的操作,避免Future中的命令式编程

4.Optional:能显式表示缺失值。正确使用能够发现数据缺失的原因。还有一些与Stream类似的方法。

5.默认方法

Java 8 实战 P4 Beyond Java 8的更多相关文章

  1. Java 8 实战 P3 Effective Java 8 programming

    目录 Chapter 8. Refactoring, testing, and debugging Chapter 9. Default methods Chapter 10. Using Optio ...

  2. 【Java】实战Java虚拟机之五“开启JIT编译”

    今天开始实战Java虚拟机之五“开启JIT编译” 总计有5个系列 实战Java虚拟机之一“堆溢出处理” 实战Java虚拟机之二“虚拟机的工作模式” 实战Java虚拟机之三“G1的新生代GC” 实战Ja ...

  3. selenium2 Webdriver + Java 自动化测试实战和完全教程

    selenium2 Webdriver + Java 自动化测试实战和完全教程一.快速开始 博客分类: Selenium-webdriverselenium webdriver 学习selenium ...

  4. Java反射实战

    一.背景 最近的项目中需要使用到Java 反射的知识,以前不怎么了解,也基本没怎么用过,抽出一片时间,来具体学习和实战下Java的反射!拿来和大家分享以及记录方便以后学习! 二.反射相关概念解析 1. ...

  5. 实战突击: Java Web项目整合开发(PDF)

    实战突击:  Java  Web项目整合开发(PDF)

  6. Java 10 实战第 1 篇:局部变量类型推断

    现在 Java 9 被遗弃了直接升级到了 Java 10,之前也发过 Java 10 新特性的文章,现在是开始实战 Java 10 的时候了. 今天要实战的是 Java 10 中最重要的特性:局部变量 ...

  7. Java日志实战及解析

    Java日志实战及解析 日志是程序员必须掌握的基础技能之一,如果您写的软件没有日志,可以说你没有成为一个真正意义上的程序员. 为什么要记日志? •       监控代码 •       变量变化情况, ...

  8. Java 8 实战 P1 Fundamentals

    目录 Chapter 1. Java 8: why should you care? Chapter 2. Passing code with behavior parameterization Ch ...

  9. Java编程实战宝典PDF (中文版带书签)

    Java编程实战宝典PDF 目录 第1篇 Java基础知识入门第1章 Java的开发运行环境( 教学视频:57分钟)1.1 Java运行原理与Java虚拟机1.1.1 Java运行原理简述1.1.2 ...

随机推荐

  1. 六时车主 App iOS隐私政策

    本应用尊重并保护所有使用服务用户的个人隐私权.为了给您提供更准确.更有个性化的服务,本应用会按照本隐私权政策的规定使用和披露您的个人信息.但本应用将以高度的勤勉.审慎义务对待这些信息.除本隐私权政策另 ...

  2. 【译】x86程序员手册00 - 翻译起因

    从上一次学习MIT的操作系统课程又过去了一年.上次学习并没有坚持下去.想来虽有种种原因,其还在自身无法坚持罢了.故此次再鼓起勇气重新学习,发现课程都已由2014改版为2016了.但大部分内容并没有改变 ...

  3. java浅析

    基本结构 1.以字节码的方式运行在虚拟机上,不是直接编译成机器码运行,所以性能上差于 C 但是高于 python这样的解释形语言. 笔者大学期间学习过 汇编和C,工作后使用python,对这两种语言有 ...

  4. HDU_1285_拓扑排序(优先队列)

    确定比赛名次 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Subm ...

  5. id拼接保存到单个字段后作为表连接的查询条件

    SELECT q.id, concat(q. NAME) qname, d.id did, d. NAME FROM question_po q LEFT JOIN data_configuratio ...

  6. 6 个 Linux 运维典型问题,大牛的分析解决思路在这里

    作为一名合格的 Linux 运维工程师,一定要有一套清晰.明确的解决故障思路,当问题出现时,才能迅速定位.解决问题,这里给出一个处理问题的一般思路: 重视报错提示信息:每个错误的出现,都是给出错误提示 ...

  7. redis --------- 使用命令(每天一个)

    Key(键) Del     语法:DEL  Key [key ...]     删除给定的一个或者多个key  不存在的key会被忽略. 返回值: 被删粗key的数量# 删除单个 key redis ...

  8. uva 1586 Molar mass(Uva-1586)

    这题做的相当的复杂...之前做的现在应该能简单一点了写的. 我的代码: #include <bits/stdc++.h> using namespace std; main() { int ...

  9. node.js 中的package.json文件怎么创建?

    最近在用webstorm和nodejs做一些东西,老是各种混乱,今天上午创建一个新的项目,结果发现,npm init之后,并没有出现package.json,并没有太明确他的功能的小姑娘表示十分的惊慌 ...

  10. linux学习7-数据流重定向

    数据流重定向 实验介绍 你可能对重定向这个概念感到些许陌生,但你应该在前面的课程中多次见过>或>>操作了,并知道他们分别是将标准输出导向一个文件或追加到一个文件中.这其实就是重定向, ...