「Java 8 函数式编程」读书笔记——高级集合类和收集器
原文链接 http://blog.gaoyuexiang.cn/Java_8_Lambdas_Functional_Programming_Note_Collector/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
本章是该书的第五章, 主要讲了方法引用和收集器
方法引用
形如:
artist -> artist.getName()
(String arg) -> arg.length()
这样的表达式, 可以简写为:
Artist::getName
String::length
这种简写的语法被称为方法引用
. 方法引用无需考虑参数, 因为一个方法引用可以在不同的情况下解析为不同的Lambda
表达式, 这依赖于JVM
的推断.
方法引用的类型
方法引用可以分为四类:
- 引用静态方法:
ClassName::staticMethodName
, 比如:String.valueOf
- 引用特定实例方法:
object::instanceMethodName
, 比如: str::toString - 引用特定类型的任意对象的实例方法:
ClassName::instanceMethodName
, 比如:String::length
- 引用构造方法:
ClassName::new
, 比如:String::new
元素顺序
当我们对集合进行操作时, 有时希望是按照一定的顺序来操作, 而有时又希望是乱序的操作. 有两个方法可以帮助我们进行顺序的操作.
乱序
BaseStream.unordered()
方法可以打乱顺序, 科技将本来有序的集合变成无序的集合
排序
Stream.sorted
方法有两个签名, 一个无参, 一个有参数Comparator<? super T> comparator
- 无参的方法要求
T
实现了Comparable
接口 - 有参方法需要提供一个比较器
收集器
收集器是一种通用的, 从流中生成复杂值的结构. 将其传给collect
方法, 所有的流就都可以使用它. 而下面提到的单个收集器, 都可以使用reduce
方法模拟.
转换成集合
我们可以使用Collectors
中的静态方法toList()
toSet()
等, 将流收集为List
或Set
stream.collect(toList())
stream.collect(toSet())
我们不需要关心具体使用的是哪一种具体的实现, Stream
类库会为我们选择. 因为我们可以利用Stream
进行并行数据处理, 所以选择是否线程安全的集合十分重要.
当然我们也可以指定使用哪一种实现来进行收集:
stream.collect(toCollection(ArrayList::new))
转换成值
Collectors
类提供了很多的方法用于转化值, 比如counting
maxBy
minBy
等等, 可以查看javadoc
了解.
目前了解到的是, 这三个方法都可以使用Stream
中的count
max
min
方法代替, 而不需要作为collect
方法的参数
数据分割
有时我们想按照一个条件把数据分成两个部分, 而不是只获取符合条件的部分, 这时可以使用partitioningBy
方法收集. 将它传入collect
方法, 可以得到一个Map<Boolean, List>
, 然后就可以对相应的数据进行处理了.
数据分组
groupingBy
方法可以将流分成多个List
, 而不仅仅是两个, 接收一个Lambda
表达式作为参数, 其返回值作为key
, 最后的结果也是一个Map
, 形如Map<String, List>
. 这一方法类似于SQL
中的group by
生成字符串
如果要从流中得到字符串, 可以在得到Stream<String>
之后使用Collectors.joining
方法收集. 该方法接收3个String
参数, 分别是分隔符
前缀
后缀
artists.stream()
.map(Artist::getName)
.collect(Collectors.joining(",", "[", "]"));
组合收集器
我们可以将收集器组合起来, 达到更强的功能. 书上举了两个栗子:chestnut:
- 例一
public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {
return albums
.collect(
groupingBy(Album::getMainMusicina, counting()));
}
这个方法的目的是统计每个歌手的作品数目. 如果不组合收集器, 我们先用groupingBy
得到一个Map<Artist, List<Album>>
之后, 还要去遍历Map
得到统计数目, 增加了代码量和性能开销.
上面的counting
方法类似于count
方法, 作用于List<Album>
的流上.
- 例二
public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums) {
return albums
.collect(
groupingBy(Album::getMainMusician,
mapping(Album::getName, toList())));
}
这个方法的目的是得到每个歌手的作品名称列表. 如果不组合收集器, 我们将会先得到一个Map<Artist, List<Album>>
. 然而, 我们只想得到作品名称, 也就是一个List<String>
, 组合mapping
收集器可以帮助我们实现效果.
mapping
收集器的功能类似于map
, 将一种类型的流转换成另一种类型. 所以类似的, mapping
并不知道要把结果收集成什么数据结构, 它的第二个参数就会接收一个普通的收集器, 比如这里的toList
, 来完成收集.
这里的counting
和mapping
是我们用到的第二个收集器, 用于收集最终结果的一个子集, 这些收集器叫做下游收集器
.
定制收集器
定制收集器看起来麻烦, 其实抓住要点就行了.
使用reduce
方法
前面说过, 这些收集器都可以使用reduce
方法实现, 我们定制收集器, 实际上就是为reduce
方法编写三个参数, 分别是:
- identity
- accumulator
- combiner
关于这三个参数的意义, 如果不太理解, 可以看看这个答案: https://segmentfault.com/q/1010000004944450
我们可以设计一个类, 为这三个参数设计三个方法, 再提供一个方法用于获取目标类型(如果这个类就是目标类型的话, 可以不提供这个方法)
实现Collector
接口
如果不想显式的使用reduce
方法, 我们只需要提供一个类, 实现Collector
接口.
该接口需要三个泛型参数, 依次是:
- 待收集元素的类型
- 累加器的类型
- 最终结果的类型
需要实现的方法有:
supplier
: 生成初始容器accumulator
: 累加计算方法combiner
: 在并发流中合并容器finisher
: 将容器转换成最终值characteristics
: 获取特征集合
多数情况下, 我们的容器器和我们的目标类型并不一致, 这时, 需要实现finisher
方法将容器转化为目标类型, 比如调用容器的toString
方法.
有时我们的目标类型就是我们的容器, finisher
方法就不需要对容器做任何操作, 而是通过设置characteristics
为IDENTITY_FINISH
, 使用框架提供的优化得到结果.
详细讲解可以参见http://irusist.github.io/2016/01/04/Java-8%E4%B9%8BCollector/
Map
新增方法
Java 8
为Map
新增了很多方法, 可以通过搜索引擎轻松找到相关文章. 这里举几个书中提到的相关方法.
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
V computeIfPresent(K key, BiFunction<? super K, ? super V, extends V> remappingFunction)
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
这三个方法类似, 都是根据key
来处理, 只是Lambda
表达式的执行条件不同, 从函数名就可以看出来. 不过要注意Lambda
表达式的参数, 第一个方法的Lambda
只需要一个参数key
, 后面两个方法的Lambda
需要两个参数key
和value
, 而compute
方法的Lambda
中的value
参数可能为null
.
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
此方法用于合并value
, 新value
在第二个参数给出. Lambda
表达式规定合并方法, 其两个参数依次是oldValue
和newValue
, oldValue
是原Map
的value
, 可能为空; newValue
为merge
方法的第二个参数.
void forEach(BiConsumer<? super K, ? super V> action)
通过forEach
方法, 不再需要使用外部迭代来遍历Map
.