Java 函数式编程

函数式编程

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

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

  • 首先是一个 Lambda 表达式的接口 XXFunction ,表达了这个是一个 Lambda 表达式
  • 然后使用变量 fn 来表示这个函数的对象
  • 紧接着在等号后面使用 () 表达了这个函数的参数
  • 使用 -> {} 来表达这个函数到底是做什么
1
2
3
4
5
6
7
8
9
java复制代码// 普通函数
public static void fn(T param1,R param2){
// ······
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 完整表达式 =>
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 个部分组成

1
2
3
4
5
6
7
8
9
java复制代码// 非必须,若加上则编译期会提供校验
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Entity {
String msg = "";

public Entity() {

}

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

public static Entity getInstance() {
return new Entity();
}

}

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

1
2
3
4
5
6
7
8
9
java复制代码// 引用构造方法
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); 。可以看到主要是有两个参数:第一个是初始值,第二个是累加累积函数,这个函数其实可以和换种写法更好理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 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);

1
2
3
4
5
6
7
java复制代码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函数

1
2
java复制代码//第一个参数初始值,第二个参数累积累积函数,第三个函数累积累积后怎么与初始值进行合并
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码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操作可变数据

他们的区别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 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的实体类和元组类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码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操作,先按编程语言,再按编程等级分层,然后返回一个元组<平均工资,程序员列表>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java复制代码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对我们的操作影响

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public 获取学生(){
if(学校!=null){
年级=学校.get();
if(年级!=null){
班级=年级.get();
if(班级!=null){
小组=班级.get();
if(小组!=null){
学生=小组.get();
return 学生
}
}
}
}
}

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

1
2
3
4
5
6
7
8
java复制代码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 相关的知识了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%