Java8函数式编程--编程范式的趋势

让我们来说说函数式编程–Lambda表达式

上周开始,看了这本书–Java 8 函数式编程,以下是我的读书摘抄。

背景知识:

函数接口:只有一个抽象方法的接口

方法签名:由方法名+形参列表构成+返回值类型。JVM是并没有特别明确的将数据类型写出来,而是提供了特殊的表示法。

惰性求值方法: 只描述Stream,不产生实际执行的方法

及早求值: 立刻执行

函数的副作用:当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响(例如修改全局变量或修改参数)

纯函数:输入输出数据流全是显式的(显式的含义是:函数与外界交换数据只有一个唯一渠道——参数和返回值;函数从函数外部接受的所有输入信息都通过参数传递到该函数内部;函数输出到函数外部的所有信息都通过返回值传递到该函数外部)

非纯函数:与纯函数相反,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如,读取全局变量,修改全局变量。

阿姆达尔定律:对于固定负载情况下描述并行处理效果的加速比s,S=1/(1-a+a/n),其中a为并行计算部分所占比例,n为并行处理结点个数

正文

Lambda表达式

在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值

Lambda表达式

  1. 传入一段代码块
  2. 不需要显式声明/指定参数类型(编译器可以通过方法签名来推断类型)

Lambda表达式引用的局部变量必须是final或既成事实上的final变量(基本类型只能赋值一次,对象的引用不能改变)

Stream API

意义和创新:遍历从外部迭代变成了内部迭代。把程序扮演Director导演类的部分逻辑和操作提取、转移到了类库中

实现机制

Stream操作分为惰性求值和及早求值;形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果。

整个过程被分解为多个惰性求值操作和一个单个及早求值操作,而这些多个操作只需要遍历一次。整个过程和建造者模式有共同之处。建造者模式使用一系列操作来设置属性并配置,最后调用build方法,这时,对象才被真正创建。

常用的流操作

collect:流生成集合

map:改变流中元素的类型

flatMap:把最初流中的一个元素转成一个流

filter:过滤流中的元素

Max/Min:求极值,返回 Optional (涉及比较器类Comparator,因为比较意味着排序,排序意味着需要一个排序的指标)

reduce

Lambda表达式正确使用姿势

1)明确要达成什么转化,而不是说明如何转化

2)写出的函数尽量没有副作用

纯函数的优点
  1. 无状态,线程安全。不需要线程同步。
  2. 纯函数相互调用组装起来的函数,还是纯函数。
  3. 可缓存结果,原生支持并行(不懂)

I/O API可以看作是一种特殊的全局变量。文件、屏幕、数据库等输入输出结构可以看作是独立于运行环境之外的系统外全局变量,而不是应用程序自己定义的全局变量。

类库

装箱类型在Lambda表达式上的体现

  1. int整型在内存中占用4个字节,Integer在内存中占用16字节;
  2. 基本类型和装箱类型相互转化需要额外的计算开销
  3. 在Java中想要一个包含整型值的列表List,实际上得到的却是一个包含整型对象的列表List

对于需要大量数值运算的算法来说,由于上述的原因,减缓了程序的运行速度

因此,Stream类的某些方法对基本类型和装箱类型做了区分;Java 8对整型,长整型和双浮点型做了特殊处理;方法有:mapToLong(),MapToInt();

Lambda表达式的重载

Lambda表达式的类型就是对应的函数接口类型。遇到重载时调用准则:

  1. 优先执行最具体的类型
  2. 如果最具体的类型有多个,则需要认为指定类型

默认方法

Java 8新增一个关键词default,在接口中,可以存在由default修饰的默认方法(也就是说,Java 8接口里,不再只能是抽象方法,具体的方法也可以存在即默认方法;同时,也允许存在静态方法),当子类不实现这个接口的某个抽象方法时,子类就是使用该默认方法。

还要注意一点:一个类要实现的接口里,存在默认方法和它父类的方法相同,则优先选择父类定义的方法。

归纳出两条简单的定律:

  1. 类胜于接口。如果在继承链中有方法或抽象方法声明,那么久可以忽略接口中定义的方法
  2. 子类胜过父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认的方法,那么子类中定义的方法胜出。

Optional

该新的数据类型,用来替换null。Optional对象相当于值的容器,类似ThreadLocal用法

  1. 创建Optional对象:Optional.of()
  2. 获取值:get();
  3. 获取可能存在的空值:empty().
  4. 判断Optional对象理是否有值:isPresent();
  5. orElse(T t),当Optional对象为空时,提供一个备选值t
  6. orElseGet(Supplier supplier):当Optional对象为空时,调用这个函数接口

特殊的修饰

@FunctionalInterface

该注释会强制javac检查一个接口是否符合函数接口的标准(一个接口,只有一个抽象方法),如果该注释添加给一个枚举类型,类或另一个注释,或者接口含不止一个抽象方法,javac就会报错(编译的时候)。重构代码时,使用它能很容易发现问题。

高级集合类

方法引用

本小节摘自【译】Java 8的新特性—终极版

方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。

西门的例子中,Car类是不同方法引用的例子,可以帮助读者区分四种类型的方法引用。

public static class Car {
public static Car create( final Supplier< Car > supplier ) {
    return supplier.get();
}              

public static void collide( final Car car ) {
    System.out.println( "Collided " + car.toString() );
}

public void follow( final Car another ) {
    System.out.println( "Following the " + another.toString() );
}

public void repair() {   
    System.out.println( "Repaired " + this.toString() );
}
}

第一种方法引用的类型是构造器引用,语法是Class::new,或者更一般的形式:Class::new。注意:这个构造器没有参数。

final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );

第二种方法引用的类型是静态方法引用,语法是Class::static_method。注意:这个方法接受一个Car类型的参数。

cars.forEach( Car::collide );

第三种方法引用的类型是某个类的成员方法的引用,语法是Class::method,注意,这个方法没有定义入参:

cars.forEach( Car::repair );

第四种方法引用的类型是某个实例对象的成员方法的引用,语法是instance::method。注意:这个方法接受一个Car类型的参数:

final Car police = Car.create( Car::new );
cars.forEach( police::follow );

数据并行化

并发,并行;数据并行化;任务并行化

调用parallelstream()/parallel(),生成一个并行化流

收集器

Collectors.toList(),Collectors.toSet,Collectors.to

调试Lambda表达式

对于业务稳定,生命周期较长的产品,可以引入单元测试。

程序员写出的程序bug,有两种:

  1. 实现业务逻辑的思路错误
  2. 实现业务逻辑的思路对了,但是在实现过程中出了问题,开发者设想的过程和真实的情况不同。

软件产品种,大部分bug来源于第二种。我们对单元测试,下一个定义:单元测试是测试一段代码的行为是否符合预期的方式。我们对每一个方法输出结果都存在一个预期,这个预期是我们在实现业务过程中的一个环节,这些环节全部累加起来,每个环节的结果都是我们想要的,那么这个业务我们就能正确的实现了。因此,单元测试,能检测出第二种bug;

对重构Lamdba表达式的思考

如果存在多个方法,每个方法的处理过程相同,输入输出类型相同,不同的是操作的具体逻辑,则可以把相同的操作提取成新的方法,不同的操作通过方法入参来体现。比如下面两个方法:

计算音乐家数量

public long countMusicians(){
    return albums.stream()
                    .mapToLong(albu ->.getMusicians().count)
                    .sum()
}

计算单曲数量

public long countTracks(){
    return albums.stream()
                    .mapToLong(albu ->.getTracks().count)
                    .sum()
}    

要怎么重构呢?

首先我们定义一个函数接口,输入Album类,输出一个long型, 引入泛型(Java 8其实已经定义了这种输入输出的接口了)

public interface ToLongFunction<T>{
    long applyAsLong(T value);
}

我们知道,Lambda表达式返回类型是函数接口,因此上述的两个方法就可以重构成下面的样子

public long countMusicians(){
    return countFeature(ablum -> album.getMusicians().count())
}

public long countTracks(){
    return countFeature(ablum -> ablum.getTracks().count())
}

private long countFeature(ToLongFuction<Album> function){
    return albums.stream()
                    .mapToLong(function)
                    .sum();
}

整个思考过程,和非函数式编程是类似的。

对惰性求值方法的调试

Java 8 提供了一个方法peek(),举个例子

Stream.of("one", "two", "three", "four")
        .peek(e -> System.out.println("Peeked value: " + e))
        .map(String::toUpperCase)
        .peek(e -> System.out.println("Mapped value: " + e))
        .collect(Collectors.toList());

这样就能对流的每一个元素处理的先后值的变化进行观察了