Java函数式编程

函数式编程

什么是函数式编程?
  • 一种编程范式
  • 函数作为第一对象
  • 注重描述而不是执行步骤
  • 关心函数之间的关系
  • 不可变
Lambda 表达式

以下的代码展示了一个普通的函数和一个 Lambda 表达式的不同之处

  • 首先是一个 Lambda 表达式的接口 XXFunction ,表达了这个是一个 Lambda 表达式

  • 然后使用变量 fn 来表示这个函数的对象

  • 紧接着在等号后面使用 () 表达了这个函数的参数

  • 使用 -> {} 来表达这个函数到底是做什么

// 普通函数
 public static void fn(T param1,R param2){
     // ······
 }

// Lambda 
XXFunction fn = (T param1,R param2) -> {
    // ······
}
复制代码
Lambda 表达式语法糖

如果每一个 Lambda 表达式都需要完整的写出以上4个部分,未免比较冗余。

因此 JDK 提供了3个语法糖来简化 Lambda 表达式,以下四种写法展示了 Lambda 的三种语法糖

// 完整表达式 =>
public Function<Integer, Integer> function = (Integer a) -> {
    return a * a;
};
// 单行可省略大括号 =>
public Function<Integer, Integer> function1 = (Integer a) -> (a * a);

// 参数类型可推导 =>
public Function<Integer, Integer> function2 = a -> {
    return a * a;
};
// 单参数可省略小括号
public Supplier<Integer> function3 = () -> 1;
复制代码
自定义函数式接口

以下的代码展示了怎么自定义一个函数式接口,主要是由 3 个部分组成

// 非必须,若加上则编译期会提供校验
@FunctionalInterface

// 必须声明为 interface
public interface LambdaExample<T> {

    // 单个非默认/静态实现方法
    public abstract T apply();
}
复制代码
内置常用函数式接口

为了简化开发,JDK 已经提前声明了一些常用的 Lambda 表达式,如下所示

输入 返回值 Class 备注
T R Function<T,R> 一个输入和输出,通常是对一个值操作返回一个值
void T Supplier< T > 只有返回值,通常作为生产者
T void Consumer< T > 只有输入值,通常作为消费者
void void Runnable 即无输入也无输出,单纯的执行
T Boolean Predicate< T > 通常用于对输入值进行判断,Function的特殊形式
T T UnaryOperate< T >

如果我们有以上场景,则直接使用 JDK 提供的内置接口即可

方法引用

如果定义了一个 Lambda 函数,需要怎么才能把一个普通的方法引用到 Lambda 表达式上呢?

如下,先定义一个 Entity

public class Entity {
    String msg = "";

    public Entity() {

    }

    public Entity getEntity() {
        return new Entity();
    }

    public static Entity getInstance() {
        return new Entity();
    }
    
}
复制代码

JDK 提供三种方式来引用一个方法,如下所示

// 引用构造方法
LambdaExample<Entity> example = Entity::new;

// 引用静态方法
LambdaExample<Entity> example1 = Entity::getInstance;

// 指定实例的方法
Entity entity = new Entity();
LambdaExample<Entity> example2 = entity::getEntity;
复制代码

通过上面的方法,就可以把一个 Lambda 表达式和一个普通方法绑定了

函数式接口转换

由于 Java 是强类型,在某些场合下,并不要求函数签名完全一致,可以进行转换,例如:

  • 忽略输入:Function <- Supplier
  • 忽略返回:Consumer <- Function
  • 忽略输入和返回: Runnable <- Supplier
Stream

作为函数式编程的最常用的地方,在已经拥有 List 的情况下,为什么还要引入 Stream 呢?

  • Stream 可以是无限的
  • Stream 可以并行处理
  • Stream 可以延迟处理

如何创建一个 Stream ?

  • 静态数据 Stream.of()
  • 容器 collection.stream()
  • 动态 Stream.iterate() & Stream.generate()
  • 其他 API Files.lines()
Stream 基本操作

stream 操作分为两类,分别是中间操作和结束操作,他们在整个 Stream 操作中的关系图如下

Source => Intermediate Operation => Intermediate Operation => ...... => Intermediate Operation => Terminal Operation => Result

中间操作( Intermediate Operation )

  • filter
  • distinct
  • skip
  • limit
  • map/flatMap
  • sorted

结束操作( Terminal Operation )

  • count/sum
  • collect/reduce
  • forEach
  • anyMatch/allMatch/noneMath
函数式编程三板斧
  • filter(Predicate predicate)
  • map(Function mapper)
  • reduce(U identity, BinaryOperator acc)

其中,三者的作用通过下图形象的表示出来

图1.png

其中,map 和 filter 比较容易理解:map 是一种映射关系,比如将水果映射成水果块;filter 是过滤,通过条件选出符合要求的;而 reduce 较为抽象,是一种将元素混合累积的概念,比如上图将各种水果切块混成我们想要的沙拉,如下节所示

reduce 理解

选取 Stream 中 reduce 函数 T reduce(T identity, BinaryOperator<T> accumulator); 。可以看到主要是有两个参数:第一个是初始值,第二个是累加累积函数,这个函数其实可以和换种写法更好理解

// reduce 函数
T reduce(T identity, BinaryOperator<T> accumulator);
// 其中accumulator可以看做如下动作
BinaryOperator<T> accumulator = (acc, curr) -> {
    // do some
    retuen newAcc;
}

//整个上面两个动作可以用如下代码等价替换
R acc = identity;
for ( T curr: datas){
    // apply = do some
    acc = accumulator.apply(acc,curr);
}
return acc;
复制代码
reduce 例子

通过几个例子,可以更深入的了解 reduce 的作用

  • reduce 求和

    求和使用到了上节所示的 T reduce(T identity, BinaryOperator<T> accumulator);

    public static int sum(Collection<Integer> list) {
    	return list.stream().reduce(0, (acc, curr) -> acc + curr);
    }
    public static void main(String[] args) {
    	List<Integer> list = Arrays.asList(2,3,4,7,5);
    	System.out.println(sum(list));
    }
    复制代码

    其中因为是求和,第一个参数初始值直接传入0即可,第二个累加累积函数直接传入两个 int 相加即可

  • reduce 实现 map

    为了实现 map ,使用了 如下的需要传入3个参数的reduce函数

    //第一个参数初始值,第二个参数累积累积函数,第三个函数累积累积后怎么与初始值进行合并
    <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
    复制代码
    public static <T, R> List<R> map(List<T> list, Function<T, R> mapFn) {
        // 首先声明一个映射后的返回 List
    	List<R> ret = new ArrayList<>();
        // 此时的初始值可以传入 ret List,我们需要对每个元素进行操作添加到该list中
    	return list.stream().reduce(ret, (acc, curr) -> {
    				R newValue = mapFn.apply(curr);
    				acc.add(newValue);
    				return acc;
    			},
    			(list1, list2) -> {
    				list1.addAll(list2);
    				return list1;
    			});
    }
    public static void main(String[] args) {
    	List<Integer> list = Arrays.asList(2,3,4,7,5);
    	List<Integer> list2 = map(list1, i->i*2);
    	System.out.println(list2);
    	
    }
    复制代码

    此时想用 reduce 实现对一个 List 的各元素乘以2的映射动作。首先是需要给 map() 传入两个参数,一个是需要进行映射的 list ,第二个是进行 map 的函数。在reduce操作中传入了三个参数,第一个是初始值,就是我们需要进行reduce操作的函数,在整个例子中传入 i->i*2 ,第三个函数就是我们加完一个后,需要对两个list进行合并,这里就使用allAll就行

Stream.collect()

collect是汇合Stream元素的操作,在已经拥有了reduce函数,为什么还需要collect函数?

  • reduce操作不可变数据
  • collect操作可变数据

他们的区别如下:

// reduce
R acc = identity;
for ( T curr: datas){
    acc = accumulator.apply(acc,curr);
}
return acc;

// collect
R container = supplier.get();
for ( T curr: datas){
    accumulator.accept(container,curr);
}
return container;
复制代码

collect函数提供了两种参数类型

  • collect(Supplier, Accumulator, Combiner)
  • collect(Collector)

其中 Collector 是 JDK 针对于例如List/Map等常用场景提供了一个覆盖了Supplier,Accumulator,Combiner的集合,所以重点还是要落在解析collect(Supplier, Accumulator, Combiner)

通过一个图充分展现 collect 三个参数的作用

collect

从图中可以清楚的看到Collector的要素:

  • Supplier:累积数据构造函数,通过get方法获得累积结果的容器
  • Accumulator: 累积函数,通过accept对结果进行累积
  • Combiner: 合并函数,并行处理场合下用
  • Finisher: 对累积数据做最终转换,例如对最后的结果进行加1的操作
  • *Characteristics: 特征(并发/无序/无finisher)
Collectors API
  • toList/to(Concurrent)Map/toSet/toCollection
  • counting/averagingXX/joining/summingXX
  • groupBy/partitioningBy
  • mapping/reducing

其中 toXXX 是针对容器的常用场景,已经封装好一系列函数,counting等也较为容易理解,重点关注groupBy

Collectors.groupingBy

groupBy有三种用法:

  • groupingBy(Function) – 单纯分key存放成Map,默认使用HashMap
  • groupingBy(Function, Collector) - 分key后,对每个key的元素进行后续collect操作,其中Collector还可以继续进行groupingBy操作,进行无限往下分类
  • groupingBy(Function, Suppiler, Collector) - 同上,允许自定义Map创建
Collectors.groupingBy例子

定义一个Programmer的实体类和元组类

public class Programmer {
	private String name;
	private int level;
	private int salary;
	private int output;//from 1-10
	private String language;
}
public class Turple<L,R>{
	private final L l;
	private final R r;
	
	public Turple(L l, R r) {
		this.l = l;
		this.r = r;
	}
	@Override
	public String toString() {
		return "(" + l + ", " + r + " )";
	}
}
复制代码

通过groupingBy操作,先按编程语言,再按编程等级分层,然后返回一个元组<平均工资,程序员列表>

public static Map<String, Map<Integer, Turple<Integer, List<Programmer>>>> classify(List<Programmer> programmers) {
	return programmers.stream().collect(
        	// 首先把程序员语言语言分类
			Collectors.groupingBy(Programmer::getLanguage,
                    // 再这里已经通过语言分好的类后,可以继续使用groupingBy对同一语言的不同等级程序员进行分类
					Collectors.groupingBy(Programmer::getLevel,
                            // 这里collectingAndThen主要接受两个参数,一个是Collector,第二个是Collector完成后进行一个附加操作,这里就是把同一等级的程序员的工资进行一个平均
							Collectors.collectingAndThen(
									Collectors.toList(),
                                	// 此处的list就已经是同一语言同一等级的程序员list,再对该list进行求工资平均值操作
									list -> new Turple(list.stream().collect(Collectors.averagingInt(Programmer::getSalary)), list)
							)
					)
			));
}
复制代码
Optional

optional也是函数式编程中常用的,平时使用可能只是简单的判断有没有为空等操作,实际上它跟Stream一样也是函数式编程重要的组成部分

  • Stream表达的是函数式编程中,一系列元素的处理
  • Optional表达的是函数编程中,元素有和无的处理
Optional API
  • orElse(T) => if (x!= null) return x; else return T;
  • orElseGet(fn) => if (x!=null) return x else return fn();
  • ifPresent(fn) => if (x!= null) fn();
Optional.map()

optional和stream一样,同为函数式编程的组成部分,都有map操作,通过optional.map(),我们可以很精妙的避免null对我们的操作影响

例如这样一个数据结构:学校->年级->班级->小组->学生,如果要获取一个学生,则需要进行下列的一系列操作

public 获取学生(){
	if(学校!=null){
            年级=学校.get();
            if(年级!=null){
        	班级=年级.get();
        	if(班级!=null){
                    小组=班级.get();
                    if(小组!=null){
                	学生=小组.get();
                	return 学生
                    }
        	}
            }
	}
}
复制代码

而使用Optional,则为如下调用方式

public 获取学生(){
    return Optional.ofNullable(学校)
                    .map(学校::get)
                    .map(年级::get)
                    .map(班级::get)
                    .map(小组::get)
                    .orElse(null)
}
复制代码

相比起来,就会优雅很多,这里体现了map的一个运行机制
optional.png

Functor & Monad

为什么 Stream 和 Optional 都有类似的map操作?这里涉及到Stream 和 Optional都属于函数式编程中基本的模型,其他的模型如下:

  • Optional:null Or T
  • Stream:0...n
  • Either:A or B ( JDK 未实现)
  • Promise: ( JDK 未实现)
  • IO:IO operation

为什么这些模型都有类似的操作,这就属于 Functor & Monad 相关的知识了