Effective Java 学习笔记45-48 Stream
目录
Stream的基本介绍
Stream的创建
Stream的操作
中间操作(都是非静态方法)
终端操作(都是非静态方法)
谨慎使用Stream
不要滥用Stream,该封装要封装
优先选择Stream中无副作用的函数
Stream要优先使用Collection作为返回类型
谨慎使用Stream并行
本文先介绍Stream的基本概念以及Java的实现方式。后续介绍书中的几个建议。
Stream的基本介绍
Stream(流)是一个来自数据源的元素队列,这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。流有两个重要的概念:
- 数据源:流的来源,可以是集合、数组、I/O channel等。
- 操作:操作根据位置分为中间操作(将Stream依旧映射为Stream)和终端操作(将Stream映射为其他数据类型),可以根据操作的目的分为聚合操作、转换操作和统计操作等
在Java中Stream是一个接口,在接口中定义了大量的操作方法,同时这个接口是一个泛型接口,用于指定传输元素的数据类型。
Stream的创建
Stream的创建最重要的无非是确定其数据源,这也和前文提到的Stream合法数据源有紧密的关系。
集合:任何实现了Collection接口的集合都可以通过stream()创建stream
List<String> list = Arrays.asList("A","B","C");
Stream<String> stream = list.stream();
数组:Arrays有Stream静态方法
String[] array = new String[]{"A", "B", "C"};
Stream<String> stream = Arrays.stream(array);
I/O channel:通过I/O channel创建,比如Files.lines方法返回Stream,类似的还有BufferedReader等。
Path path = Paths.get("file.txt");
Stream<String> stream = Files.lines(path);
除了借用其他接口的方法来创建Stream,Stream接口本身也有丰富的创建方法:
Stream.of(T... values)
:用来创建一个包含给定值的 Stream。
Stream<String> stream = Stream.of("A", "B", "C");
Stream.generate(Supplier<T> supplier)
:通过一个生成元素的方法创建无限长度的Stream(因为长度无限,所以需要声明一些限制方法)
Stream<Integer> stream = Stream.generate(Math::random).limit(10);
//limit方法限制产生10个随机数后停止
Stream.builder<T>:允许逐个手工添加元素:
Stream.Builder<String> builder = Stream.builder();
builder.accept("A");
builder.accept("B");
Stream<String> stream = builder.build();
Stream.iterate(T seed, UnaryOperator<T> f)
:
通过迭代的方式产生无限长度Stream
Stream<Integer> stream = Stream.iterate(2, n->n+2);
通过以上这些方法,我们可以将集合、数组、I/O channel,甚至一个表达式的执行结果组合成一个Stream用于后续的处理。
Stream的操作
中间操作(都是非静态方法)
中间操作将一个Stream映射为另一个Stream(类似于UnaryOperator<T>方法),这些中间操作组合起来就形成了Stream的流水线(pipeline)。
类别 | 方法详情 | 介绍 |
过滤 | filter(Predicate<T> predicate) | 筛选出满足条件的元素。 |
映射 | map(Function<T, R> mapper) : | 将 Stream 中的每个元素映射为另一个类型的元素。 |
flatMap(Function<T, Stream<R>> mapper) | 将 Stream 中的每个元素映射为另一个 Stream,然后将这些 Stream 合并为一个 Stream。 | |
排序 | sorted() | 按自然顺序排序。 |
sorted(Comparator<T> comparator) | 按指定比较器排序 | |
分区 | distinct() | 去重 |
限制 | limit(long maxSize) | 限制 Stream 中元素的数量 |
跳过 | skip(long n) | 跳过前 n 个元素 |
并行流 | parallel() | 将 Stream 转换为并行流。 |
sequential() | 将并行流转换为顺序流 |
终端操作(都是非静态方法)
终端操作将 Stream 映射为其他数据类型或结果。
分类 | 方法详情 | 介绍 |
收集 | collect(Collector<T, A, R> collector) | 将 Stream 中的元素收集到集合或其他容器中 |
归一 | reduce(BinaryOperator<T> accumulator) | 将 Stream 中的元素归约为一个值 |
reduce(T identity, BinaryOperator<T> accumulator) | 带有初始值的归约 | |
遍历 | forEach(Consumer<T> action) | 遍历 Stream 中的每个元素并执行某个操作。 |
匹配 | anyMatch(Predicate<T> predicate) | 检查 Stream 中是否存在至少一个元素满足条件 |
| 检查 Stream 中的所有元素是否都满足条件。 | |
noneMatch(Predicate<T> predicate) | 检查 Stream 中是否有任何元素不满足条件。 | |
查找 | findFirst() | 查找 Stream 中的第一个元素。 |
findAny() | 查找 Stream 中的任意一个元素(在并行流中可能不是第一个元素)。 | |
计数 | count() | 计算 Stream 中的元素数量 |
最后有两点文中的注意事项:
- Stream方法有lazy属性,在调用终端操作之前,中间操作不会执行。
- Stream方法可并行执行也可以顺序执行。
谨慎使用Stream
Stream API非常强大,但是如果使用不当,会使得程序变得混乱且难以维护,下面文字谈了几点使用Stream API的注意事项。
不要滥用Stream,该封装要封装
文中举了一个例子,把文档中每一个文字做一个转换(按字母顺序重新排列),然后组合成一个映射,在映射输出之前在做一个筛选(只输出文字长度大于10的文字)。这里只截取其中部分说明:
public class test {
public static void main(String[] args) throws Exception {
Path file = Paths.get(args[0]);
try(Stream<String> stream = Files.lines(file)){
stream.collect(
groupingBy(word->word.chars().sorted()
.collect(StringBuilder::new,
(sb,c)->sb.append((char)c),
StringBuilder::append)
.toString()
)
)
.values().stream()
.filter(group->group.size()>=10)
.forEach(group->System.out.println(group.size()+":"+group));
}
}
}
这个实现完全通过Stream实现,但是实话实说比较难懂,尤其是groupingBy中产生键的方法容易让人迷惑,而且有大量的括号稍有会和groupingBy的其他参数看岔(其实,这里这里使用的是groupingBy的单参数方法,所有的处理都是在产生键值),所以可以通过封装的方法把这个键值产生的方法封装成一个独立方法,然后通过Lambda或者直接函数调用的方式实现。
public class test2 {
public static void main(String[] args) throws Exception {
Path file = Paths.get(args[0]);
try(Stream<String> stream = Files.lines(file)){
stream.collect(
groupingBy(word->alphabetize(word))
)
.values().stream()
.filter(group->group.size()>=10)
.forEach(group->System.out.println(group.size()+":"+group));
}
}
public static String alphabetize(String s){
char[] chars = s.toCharArray();
Arrays.sort(chars);
return new String(chars);
}
}
优先选择Stream中无副作用的函数
这里文中重点介绍了Stream的各种API,其中对于Collector(收集器)的各种方法有比较详细的介绍,在上文介绍过Collector的目的是将 Stream 中的元素收集到集合或其他容器中,这里的归集方法可以按照集合的类型来分:
方法 | 介绍 |
toList() | 返回List |
toSet() | 返回Set |
toMap(keyMapper, valueMapper) | 返回一对一映射 |
groupingBy() | 返回一对多的映射 |
joining() | 只在CharSequence实例的Stream中操作,用于合并char |
其中toList、toSet是无参数方法,不做详细介绍,文中重点介绍的是toMap和groupingBy两个返回映射的方法。
toMap和groupingBy都有三种重载方法:
- toMap最简单的重载有两个参数,分别指定了键与值的生成规则。
- toMap第二种重载有三个参数,新增了一个合并函数,用于在多个值对应一个键的时候通过合并函数返回一个值。
- toMap第三种重载有四个参数,新增了一个映射工厂,可以指定如EnumMap或者TreeMap等特殊的映射实现
groupingBy的重载思路与toMap类似:
- 第一种重载只有一个参数,指定了分类方法(这里不同类名就是键)。
- 第二种重载有两个参数,指定了downstream方法,就是对于每一个分类的子stream要做的流处理。
- 第三种重载有三个参数,制定了映射工厂 。
最后一个joining方法也有三种重载:
- 第一种无参数,直接返回所有char组成的String
- 第二种有一个delimiter参数,返回连接后的元素,其中相邻的元素用指定的delimiter隔开
- 第三种除了delimiter分隔符以外,还可以指定前缀和后缀
Stream要优先使用Collection作为返回类型
在编写返回一系列元素的方法时,可以当作Stream返回,也可以使用迭代返回。文中作者建议如果返回的元素不多,那么请直接返回一个标准集合,或者一个定制集合。如果返回一个集合不可行,就返回一个Stream或者Iterable。
文中列举了返回Iterable对象的案例,这里就不涉及了,这里的重点是只有扩展了Iterable接口的集合才能在对应的Stream中使用迭代。
谨慎使用Stream并行
文中作者建议尽量不要使用并行Stream方法,除非能够保证计算的正确性并且加快程序的运行速度。这里详细的内容在后续说并发的时候再说。