1. 引言
上一篇文章中,我们介绍了 Streams API 是如何使用的,以及列出了 java8 中 Streams API 包含的所有操作。
java8 Streams API 详解(上) -- 入门篇
那么,这些操作具体应该如何使用呢?
本文,我们就来详细介绍一下每个操作的具体用法和例子。
2. Intermediate 操作
Intermediate 操作是 Streams 中可以重复出现的转换操作,主要功能是将作为输入的流转换为新的流进行输出
2.1 map
map 操作的功能是最为基础和常用的转换操作,他将输入流中的每个元素都通过转换操作转换为新的元素从而构成一个新的流
- 示例
下面的例子展示了通过流将原始集合中所有元素进行 3 并打印:
代码语言:javascript复制List<Integer> integers = Arrays.asList(1, 2, 3);
integers.stream().map(integer -> integer 3).forEach(System.out::println);
打印出了:
456
2.2 mapToInt/mapToLong/mapToDouble
这三个方法用于将数值流转换为 IntStream、LongStream、DoubleStream
2.2.1 IntStream、LongStream 与 DoubleStream
这三个流十分适合处理基础的数值类型,他们提供了下面四个聚合方法:
方法 | 说明 |
---|---|
rangeClosed(a,b) | 返回子序列 [a,b),左闭又开。意味着包括 b 元素,增长步值为 1 |
range(a,b) | 返回子序列 [a,b),左闭右开,意味着不包括 b |
sum | 计算所有元素的总和 |
sorted | 排序元素 |
2.2.2 示例
例如,我们要计算上面经过 3 后并且转换为 long 值的数字之和:
代码语言:javascript复制List<Integer> integers = Arrays.asList(1, 2, 3);
long numsSum = integers.stream().mapToLong(integer -> integer 3).sum();
System.out.println(numsSum);
打印出了:
1 5
2.3 flatMap
代码语言:javascript复制<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
flatMap 与 map 一样用于流的元素的转换,但区别在于 flatMap 可以通过将原有的流进行拆分与重组实现转换结果多于原元素数的效果,其最大的优势在于多个流的生成和合并
2.3.1 示例
下面的例子展示了一个嵌套 List 的扁平化:
代码语言:javascript复制List<List<String>> strings = Arrays.asList(Arrays.asList("hello world", "hello techlog", "hello everyone"),
Arrays.asList("hello world", "hello Amy", "hello Tom"));
strings.stream().flatMap(Collection::stream).forEach(System.out::println);
打印出了:
hello worldhello techloghello everyonehello worldhello Amyhello Tom
2.3.1 进阶 -- inner join
假设我们现在有若干成绩单,我们需要通过 id 进行聚合,计算出每个学生的总成绩,并且要去除有科目缺考的学生
实际上,这就是一个典型的 inner join 的需求,那么,我们如何处理呢?
通过 flatMap 这很容易实现:
代码语言:javascript复制List<Student> chineseScores = Arrays.asList(
new Student(1L, "Tom", 98, "Chinese"),
new Student(2L, "Amy", 82, "Chinese"),
new Student(3L, "Redisson", 91, "Chinese")
);
List<Student> englishScores = Arrays.asList(
new Student(2L, "Amy", 93, "English"),
new Student(3L, "Redisson", 88, "English")
);
chineseScores.stream().flatMap(
student -> englishScores.stream()
.filter(artStudent -> artStudent.id.equals(student.id))
.map(artStudent -> new Student(student.getId(), student.getName(), student.getScore() artStudent.getScore(), "All")))
.forEach(System.out::println);
打印出了:
Student{id=2, name='Amy', score=175, subject='All'} Student{id=3, name='Redisson', score=179, subject='All'}
2.4 filter
代码语言:javascript复制Stream<T> filter(Predicate<? super T> predicate);
和 map 一样,filter 是 Streams API 中使用最为频繁的操作之一
他的功能是将流中的部分元素过滤掉,上面的例子中我们已经使用过 filter 实现 inner join 中的匹配操作
下面是一个更为简单的示例,仍然是数字加 3 的例子,但我们在结果中只保留 <= 5 的元素:
代码语言:javascript复制List<Integer> integers = Arrays.asList(1, 2, 3);
integers.stream().map(integer -> integer 3).filter(integer -> integer <= 5).forEach(System.out::println);
打印出了:
45
2.5 distinct
代码语言:javascript复制Stream<T> distinct();
distinct 操作没有参数,他的功能很简单,过滤掉流中的重复元素
代码语言:javascript复制List<Integer> integers = Arrays.asList(1, 2, 3);
integers.stream().map(integer -> integer/2 3).filter(integer -> integer <= 5).distinct().forEach(System.out::println);
输出了:
34
2.6 sorted
代码语言:javascript复制Stream<T> sorted(Comparator<? super T> comparator);
Sorted 的用法很简单,他通过 Comparator 接口实现中的 compare 方法实现元素的对比
下面的例子实现了基于学生分数的排序:
代码语言:javascript复制List<Student> chineseScores = Arrays.asList(
new Student(1L, "Tom", 98, "Chinese"),
new Student(2L, "Amy", 82, "Chinese"),
new Student(3L, "Redisson", 91, "Chinese")
);
chineseScores.stream().sorted(Comparator.comparing(Student::getScore)).forEach(System.out::println);
输出了:
Student{id=2, name='Amy', score=82, subject='Chinese'} Student{id=3, name='Redisson', score=91, subject='Chinese'} Student{id=1, name='Tom', score=98, subject='Chinese'}
2.7 peek
代码语言:javascript复制Stream<T> peek(Consumer<? super T> action);
peek 并不会像上述其他操作那样对流产生影响,通常我们用 peek 在流的关键环节前后添加打印语句来观察流的实际工作,例如我们可以在上述排序流中加入 peek 打印,来观察排序是否真的有进行:
代码语言:javascript复制List<Student> chineseScores = Arrays.asList(
new Student(1L, "Tom", 98, "Chinese"),
new Student(2L, "Amy", 82, "Chinese"),
new Student(3L, "Redisson", 91, "Chinese")
);
chineseScores.stream().peek(student -> System.out.println("before sorted: " student))
.sorted(Comparator.comparing(Student::getScore))
.forEach(System.out::println);
打印出了:
before sorted: Student{id=1, name='Tom', score=98, subject='Chinese'} before sorted: Student{id=2, name='Amy', score=82, subject='Chinese'} before sorted: Student{id=3, name='Redisson', score=91, subject='Chinese'} Student{id=2, name='Amy', score=82, subject='Chinese'} Student{id=3, name='Redisson', score=91, subject='Chinese'} Student{id=1, name='Tom', score=98, subject='Chinese'}
由此说明 sorted 确实执行了我们预期的工作
2.8 limit
limit 操作用来限制流中元素的个数,例如下面的例子中截取了公差为 3 的等差数列的前 10 个数字
代码语言:javascript复制Stream.iterate(0, n -> n 3).limit(10). forEach(x -> System.out.print(x " "));
打印出了:
0 3 6 9 12 15 18 21 24 27
事实上,上述两个例子中,作为数据源的流的元素数无限多的,limit 操作可以让无限元素数量的流在有限时间内实现返回,所以 limit 操作也是一个 short-circuiting 操作
2.9 skip
skip 操作用来实现对指定书目元素的跳过
limit 操作用来截取指定数量个元素,结合 skip 操作就可以完成切片操作了,例如我们希望获取公差为 3 的等差数列的 10 到 20 个数字:
代码语言:javascript复制Stream.iterate(0, n -> n 3).skip(10).limit(10). forEach(x -> System.out.print(x " "));
打印出了:
30 33 36 39 42 45 48 51 54 57
2.10 parallel/sequential
parallel 与 sequential 分别用来将流设置为并行的或是串行的
并行的流式操作对于大规模的运算来说能够很大程度上提升运算的效率,但需要注意的是,parallel 的情况下会造成 skip、limit 操作的失效,这可能会造成程序陷入死循环
并行流的元素处理顺序是无法预知的,但可以通过 forEachOrdered 实现有序化
3. Terminal 操作
Terminal 操作用来终结一个流,因此它是流的最后一个操作,他通过一系列的操作使输入的流返回一个结果
3.1 forEach
上面我们已经看过很多 forEach 的用法了,他用来对流中所有的元素做相同的处理
例如,下面的例子打印了流中的所有元素:
代码语言:javascript复制List<Integer> numList = new ArrayList<>();
for (int i = 0; i < 30; i) {
numList.add(i);
}
numList.stream().parallel().forEach(i -> System.out.print(i " "));
打印出了:
18 9 19 26 20 21 10 16 7 8 27 13 14 17 11 28 29 12 15 3 1 2 24 0 4 5 6 25 22 23
我们可以看到由于流是并行化的,所以元素被打印的顺序是无序的,要想让他们维持流中原有的顺序,我们就需要使用 forEachOrdered
代码语言:javascript复制List<Integer> numList = new ArrayList<>();
for (int i = 0; i < 30; i) {
numList.add(i);
}
numList.stream().parallel().forEachOrdered(i -> System.out.print(i " "));
打印出了:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
3.2 toArray
toArray 用来将结果转换为 java 数组:
代码语言:javascript复制Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens = Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
System.out.println(Arrays.toString(evens));
打印出了:
[2, 4, 6]
3.3 reduce
map、reduce 是一对经典的分布式操作,map 用来提供分布式处理,reduce 用来将数据进行聚合
在 Streams API 中,reduce 也充当了组合元素的角色,它提供一个起始值(种子),然后依照运算规则,和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce
下面的例子实现了各类的组合操作:
代码语言:javascript复制// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值,返回 Optional
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F")
.filter(x -> x.compareTo("Z") > 0).reduce("", String::concat);
3.4 collect
toArray 操作用来将流中的元素收集为 java 数组,collect 操作则可以将流中的元素收集为 List、Set、Map 等集合
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().map(n -> n * n).collect(Collectors.toList());
3.5 anyMatch/allMatch/noneMatch
这三个操作让流最终返回一个 boolean 类型,通过条件判断是否有命中或全部命中或全部不命中
代码语言:javascript复制boolean hasMoreThanFive = Stream.iterate(0, n -> n 3).anyMatch(i -> i > 5);
附录 -- 参考资料
https://www.twle.cn/c/yufei/java8/java8-basic-intstream-longstream-doublestream.html
https://developer.ibm.com/zh/articles/j-lo-java8streamapi/