Java 8 实战 P4 Beyond Java 8

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.默认方法

posted @ 2018-10-29 16:57  justcodeit  阅读(151)  评论(0编辑  收藏  举报