函数式编程
什么是函数式编程?
- 一种编程范式
- 函数作为第一对象
- 注重描述而不是执行步骤
- 关心函数之间的关系
- 不可变
Lambda 表达式
以下的代码展示了一个普通的函数和一个 Lambda 表达式的不同之处
- 首先是一个 Lambda 表达式的接口
XXFunction
,表达了这个是一个 Lambda 表达式 - 然后使用变量
fn
来表示这个函数的对象 - 紧接着在等号后面使用
()
表达了这个函数的参数 - 使用
-> {}
来表达这个函数到底是做什么
1 | java复制代码// 普通函数 |
Lambda 表达式语法糖
如果每一个 Lambda 表达式都需要完整的写出以上4个部分,未免比较冗余。
因此 JDK 提供了3个语法糖来简化 Lambda 表达式,以下四种写法展示了 Lambda 的三种语法糖
1 | java复制代码// 完整表达式 => |
自定义函数式接口
以下的代码展示了怎么自定义一个函数式接口,主要是由 3 个部分组成
1 | java复制代码// 非必须,若加上则编译期会提供校验 |
内置常用函数式接口
为了简化开发,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 | java复制代码public class Entity { |
JDK 提供三种方式来引用一个方法,如下所示
1 | java复制代码// 引用构造方法 |
通过上面的方法,就可以把一个 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)
其中,三者的作用通过下图形象的表示出来
其中,map 和 filter 比较容易理解:map 是一种映射关系,比如将水果映射成水果块;filter 是过滤,通过条件选出符合要求的;而 reduce 较为抽象,是一种将元素混合累积的概念,比如上图将各种水果切块混成我们想要的沙拉,如下节所示
reduce 理解
选取 Stream 中 reduce 函数 T reduce(T identity, BinaryOperator<T> accumulator);
。可以看到主要是有两个参数:第一个是初始值,第二个是累加累积函数,这个函数其实可以和换种写法更好理解
1 | java复制代码// reduce 函数 |
reduce 例子
通过几个例子,可以更深入的了解 reduce 的作用
- reduce 求和
求和使用到了上节所示的 T reduce(T identity, BinaryOperator<T> accumulator);
1 | java复制代码public static int sum(Collection<Integer> list) { |
其中因为是求和,第一个参数初始值直接传入0即可,第二个累加累积函数直接传入两个 int 相加即可
- reduce 实现 map
为了实现 map ,使用了 如下的需要传入3个参数的reduce函数
1 | java复制代码//第一个参数初始值,第二个参数累积累积函数,第三个函数累积累积后怎么与初始值进行合并 |
1 | java复制代码public static <T, R> List<R> map(List<T> list, Function<T, R> mapFn) { |
此时想用 reduce 实现对一个 List 的各元素乘以2的映射动作。首先是需要给 map()
传入两个参数,一个是需要进行映射的 list ,第二个是进行 map 的函数。在reduce操作中传入了三个参数,第一个是初始值,就是我们需要进行reduce操作的函数,在整个例子中传入 i->i*2
,第三个函数就是我们加完一个后,需要对两个list进行合并,这里就使用allAll
就行
Stream.collect()
collect是汇合Stream元素的操作,在已经拥有了reduce函数,为什么还需要collect函数?
- reduce操作不可变数据
- collect操作可变数据
他们的区别如下:
1 | java复制代码// reduce |
collect函数提供了两种参数类型
- collect(Supplier, Accumulator, Combiner)
- collect(Collector)
其中 Collector 是 JDK 针对于例如List/Map等常用场景提供了一个覆盖了Supplier,Accumulator,Combiner的集合,所以重点还是要落在解析collect(Supplier, Accumulator, Combiner)
上
通过一个图充分展现 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 | java复制代码public class Programmer { |
通过groupingBy操作,先按编程语言,再按编程等级分层,然后返回一个元组<平均工资,程序员列表>
1 | Java复制代码public static Map<String, Map<Integer, Turple<Integer, List<Programmer>>>> classify(List<Programmer> programmers) { |
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 | java复制代码public 获取学生(){ |
而使用Optional,则为如下调用方式
1 | java复制代码public 获取学生(){ |
相比起来,就会优雅很多,这里体现了map的一个运行机制
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 相关的知识了
本文转载自: 掘金