Java8中提供了新的特性,用于实现函数式编程,本文是《Java8实战》的阅读总结
行为参数化
一种把方法作为值,在需要传递参数的地方传递它的软件开发模式,用来适应不断变化的需求,减轻工作量。
行为参数化的实现方式:
-
策略模式,将要传递的代码包裹在对象中
-
匿名类
-
Lambda
简洁表达匿名函数的一种方式-
构成:参数、箭头、主体
-
使用:
(1)Where:在传递函数式接口参数或变量类型的地方,可以应用Lambda表达式。也就是说可直接将Lambda表达式赋值给函数式接口变量,也可以在调用方法时,以函数式接口为形参的地方传递Lambda表达式。
(2)What:函数式接口是指那些只定义一个抽象方法的接口(是的,JDK8以后可以不只包含抽象方法,也可以提供带有默认实现的方法,用default标识),JDK提供的函数式接口都以@FunctionalInterface注解标注,同时也建议创建我们自己的函数式接口时也用此注解标注。
(3)How:JDK提供的函数式接口可分为两类:泛型函数接口、原始类型特化,原始类型特化是为了解决包装类型自动拆装箱带来的性能问题专门为原始类型设计。 -
场景:环绕执行
-
类型检查、推断:
检查:根据上下文确定目标类型
推断:在使用Lambda的地方,参数部分可以省去标注参数类型 类似于下面这种形式。编译器可根据上下文推断出参数的类型。(a1, a2) -> a1. getWeight(). compareTo( a2. getWeight());
-
局部变量使用:
lambda表达式可以使用外层对象的变量,需要遵循事实上的final变量的原则,原因是Lambda表达式访直接访问分配在栈上的局部变量时有可能被回收,所以访问的是副本,同时也是为了限制改变外部变量,它会阻碍并行处理。
-
-
方法引用
Lambda表达式的一种语法糖,当一个Lambda只是直接调用某个方法时可以使用方法引用表达。
说明:策略模式是已经熟知的一种设计模式,这个模式里我们抽象出策略接口,实现这个接口达到策略类,在接受策略接口形参的地方传递具体的策略对象。最终实现将代码作为参数传递到另一个方法中,也就是说延迟了方法的执行。这也许是我们最容易理解的一种设计模式。
策略模式的一个问题是如果只需要传递一次具体实现策略,那定义的实现类只被实例化了一次,代码写起来比较麻烦,这时我们可以通过匿名类的方式定义一个没有名字的类,在代码执行时实例化。
但是,匿名类存在着定义繁琐、代码不易读的问题,从而出现了Lambda表达式的方式来实现行为参数化模式。在形参为函数式接口的地方我们就可以传递Lambda表达式,其典型的表达方式是“参数->方法体”。Java8中根据参数、返回值的不同,内置了很多常用的函数式接口,同时为了避免原始类型的拆装箱效率问题,提供了对应的原始类型特化版本,当然若有必要,也可以定义自己需要的函数式接口。
Java8中提供的常用函数式接口:
注意,这些接口也同时提供了可以复合操作的方法(通过默认方法实现),用来实现多个Lambda复合处理,可以构建出功能强大、表达直观的Lambda。
而方法引用,是在Lambda表达式基础上进一步的简化,如果一个Lambda表达式的方法体仅仅是调用参数对象的一个方法,则这个表达式可以用方法引用的方式替换,例如 Person p -> p.getName() 这个表达式就可以写成方法引用的方式:Person :: getName,方法引用分为以下三类:
- 静态方法调用
ClassName :: staticMethod- 类型实例方法
(args0, rest) -> args0.instanceMethod(rest)可写成Args::instanceMethod- 上下文已存在对象的方法
person :: getName
小结
策略模式、匿名类、Lambda表达式、方法引用都是为了实现行为参数化的目的。为了减少啰嗦,让代码更易读,实质上这些并没有什么新的内容,但有了这些基础,StreamAPI的应用成为可能,通过StreamAPI,我们可以实现函数式数据处理的目的,构建很复杂也很简单的声明式编程。
StreamAPI
Java8中提供了StreamAPI,使我们可以将多个操作串联起来,形成流水线,像类似SQL语句的方式声明式的完成具体操作。我们可以不再关心内部迭代处理的实现细节,同时,实现的代码更加简洁、易读、灵活(可复合)、性能更好。
目的:解决集合处理时的两个问题
- 套路与晦涩–内部迭代
- 难以利用多核–并行处理
对比Collection
- Collection主要为了存储和访问数据
- Stream主要用于描述对数据的计算
使用方式
-
构建流
- Collection.stream() / .parallelStream()
-
中间操作:将多个流连接起来
- 筛选
- filter(predicate)
- distinct
- 切片
- limit
- skip
- 映射
- map
- flatMap
- 筛选
-
终端操作
- forEach
- 查找
- findFirst
- findAny
- 匹配
- allMatch
- firstMatch
- anyMatch
- noneMatch
- 规约
-
reduce(T identity, BinaryOperator accumulator)
组合流中的元素,得到一个值,函数式编程术语中称为折叠 -
collect(Collector)
- 分组groupingBy
- 分区partitioningBy
-
中间操作生成流对象,一般指Stream类的实例,但有时为了避免原始类型的拆装箱问题,需要将流转换为原始类型特化的流(IntStream/LongStream/DoubleStream),Stream接口提供mapToInt等方法、以及box方法实现流类型的转换。
流操作也可以按有无状态进行分类,例如filter、map操作是无状态的,而reduce、max、sum等操作需要内部状态来累积结果,则他们是有状态的,有状态中的一些操作又可按所需存储是否是有界的分为有界操作和无界操作,例如sort、distinct是无界的,而skip、limit、reduce是有界的。
collect方法接收Collector收集器参数执行规约操作,Collectors实用类提供了很多静态工厂方法可以很方便的构建出Collector,包括toList()、toSet()、joining()、summingInt()等,而这些方法是为了方便开发人员应用,其实质都可以用reducing()表示。
同为规约操作,reduce和collect的区别在于
reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设计就是要改变容器,从而累积要输出的结果。collect方法也更适合并行操作。
注意:
- 流只能遍历一次
- 短路特性
limit、findAny、skip等中间操作使用了类似于逻辑与或的短路特性 - 一些终端操作返回结果是Option<>类的对象
常用Stream操作见下表
小结
StreamAPI隐藏了内部迭代细节,充分利用Lambda表达式,使我们可以声明式的处理数据集合,StreamAPI中的中间操作可构建出一条操作流水线,而终端操作触发流水线的执行,得到最终结果,利用终端操作的规约、分组功能可以方便、易读的实现以前啰嗦的功能编码,最后,在隐藏了迭代细节的基础上,StreamAPI帮助我们更容易的实现并行操作。