Java基础:JDK8 Stream 的方法应用
JDK 8 的新特性中包含了一个新的 Stream API,可以写出高效、干净、简洁的代码。
Stream 操作将要处理的集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理, 比如筛选, 排序,归集等。
Stream 不是数据结构,并不保存数据,它的主要目的在于计算。
Lambda 和 Stream 存在的最大最大缺点是不好调试。不过 IDEA 2019 已集成了 Stream 的调试插件 Java Stream Debugger。
Stream 概述
Stream(流)是一个来自数据源的元素队列并支持聚合操作。
- 数据源:流的来源,可以是集合,数组,I/O channel,生成器 generator 等。
- 中间操作:每次返回一个用于操作的流对象。必须结合 终端操作 才会输出结果值。
- 终端操作:每个流只能执行一次终端操作,终端操作结束后流无法再次使用。终端操作会输出一个集合或一个值。
Stream 特性
- Stream 不会存储数据,而是按需计算,一般会输出结果。
- Stream 不会改变数据源,完成操作后通常会输出一个新的集合或值。
- Stream 具有延迟执行特性,只有调用终端操作时,中间操作才会执行。
Stream的创建
集合创建Stream
Java 8 中,集合接口有两个方法来创建流:
**java.util.Collection.stream()**:为集合创建串行流。
**java.util.Collection.parallelStream()**:为集合创建并行流,内部以多线程并行执行的方式对流进行操作,在多处理器服务器下具有优秀性能。
串行流还可以转换为并行流:
stream.parallel()
,返回并行的等效流,可能返回的是本身,因自身已是并行流,或流底层已被修改为并行流。
数组创建Stream
数组也可以使用流来操作:
**java.util.Arrays.stream(T[] array)**:数组使用流来操作。
1
2String[] array = {"Hello", "World"};
Stream<String> stream = Arrays.stream(array);
Stream静态方法
Stream 自身提供了静态方法来创建:of()
、iterate()
、generate()
1 | Stream<String> stream = Stream.of("Hello", "World"); |
Stream 中间操作
每次返回一个用于操作的流对象。必须结合 终端操作 才会输出结果值。
map:映射
map
:流映射是将接收到的元素映映射成另一个元素。
1 | List<String> stringList = Arrays.asList("Hello", "Word", "Welcome", "China"); |
相对于 foreach
,foreach
和 collect
都是终端操作,但 foreach
仅仅是遍历,无返回值,需要额外声明集合用于填充。
map
的局限之处在于:对于多层嵌套的源数据集,无法采用连续的 .map
的方式来对深层元素进行处理。
mapToInt:数值映射
返回一个 IntStream 它包含将给定函数应用于此流的元素的结果。这是一个中间操作。
其他数值类型的方法有:mapToLong,mapToDouble。
1 | IntStream intStream = dogList.stream().mapToInt(Dog::getAge); |
flatMap:嵌套映射
flatMap
:在 map
的基础上多做了一步,用 Stream 对象包裹了原来 map 的返回类型,支持多层嵌套的处理;而 map 只能操作第一层。如果入参的是对象,flatMap 可以操作对象的属性,而 map 只能操作到对象。
1 | Book book1 = new Book("Chinese", Arrays.asList("chapter1", "chapter2")); |
filter:过滤
filter 方法入参为一个断方条件,筛选出满足条件条件的元素,效果与 if 条件判断一致。
1 | List<User> boyList = userList.stream().filter(user -> user.getSex().equals("boy")).collect(Collectors.toList()); |
过滤再统计
1 | long girlNum = userList.stream().filter(user -> user.getSex().equals("girl")).count(); |
distinct:去重
去掉重复的元素
1 | List<String> list1 = Stream.of("a", "a", "c").collect(Collectors.toList()); |
skip:跳过
skip(long n)
:跳过流中的 n 个元素。
1 | List<String> list1 = Stream.of("a", "a", "c", "d").collect(Collectors.toList()); |
limit:限制
limit(long n)
:获取流中 n 个元素,按加入集合的顺序取。
1 | List<String> list1 = Stream.of("a", "a", "c", "d").collect(Collectors.toList()); |
sorted:排序
对集合元素进行排序,或根据集合中对象属性进行排序。
1 | // 基础类型排序 |
更多示例
1 | Integer[] ageArr = {11,5,12,16,4,15,14,17,21}; |
peek:流调试
此方法主要用于调试,在流程某个步骤查看执行元素情况。通常用于对数据监控,记录日志等。
返回一个由该流的元素组成的流,另外在每个元素上执行提供的操作,因为元素从结果流中被消耗。这是一个中间操作。
对于并行流管道,可以在上游操作使元素可用的任何时间和线程中调用该操作。 如果操作修改共享状态,则它负责提供所需的同步。
1 | List<String> result = Stream.of("one", "two", "three", "four") |
Stream 终端操作
一个流有且只能有一个终端操作,当这个操作执行完后,流就被关闭,无法再被操作。
collect:可变归约操作
该方法入参是一个 Collector
函数方法。Collectors
类是 Collector 的实现类,通过内部静态类 CollectorImpl
实现了 Collector
的接口, Collectors
相当于一个工具类,提供了多个返回 Collector
的静态方法。这是一个终端操作。
使用 Collector 对此流的元素执行可变归约操作。 Collector 封装了用作 collect(Supplier, BiConsumer, BiConsumer)
参数的函数,允许重用收集策略和组合收集操作,例如多级分组或分区。
如果流是并行的,并且Collector是concurrent ,并且流是无序的或收集器是unordered ,那么将执行并发减少(有关并发减少的详细信息,请参阅Collector 。)
当并行执行时,可以实例化、填充和合并多个中间结果,以保持可变数据结构的隔离。 因此,即使与非线程安全的数据结构(例如ArrayList )并行执行,也不需要额外的同步来进行并行缩减。
java.util.stream public interface Collector<T, A, R>
将输入元素归集到可变结果容器中的可变归约操作,可选择在处理完所有输入元素后将累积结果转换为最终表示。 归约操作可以顺序执行,也可以并行执行。
可变归约操作的示例包括:将元素累积到 Collection ; 使用 StringBuilder 连接字符串; 计算有关元素的摘要信息,例如 sum、min、max 或 average; 计算 数据透视表摘要,例如“卖方的最大价值交易”等Collectors类提供了许多常见可变归约的实现。
Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors 可用于返回列表或字符串,以下是使用预定义收集器执行常见可变归约任务的示例:
Collectors.toList:归集元素
1 | // 归集多个 name 到一个 List |
Collectors.toMap:映射到Map
返回一个将元素归集到 Map 中的 Collector ,其 key
和 value
是将提供的映射函数应用于输入元素的结果。
如果存在 key
重复(根据Object.equals(Object) ),则在执行集合操作时会抛出 IllegalStateException 。 如果映射的键可能有重复项,请改用 toMap(Function, Function, BinaryOperator)
。
Map 的 Key 和 Value 都为对象属性
1 | //Map 的 key 和 value 都为对象属性 |
Map 的 Key 为对象属性, Value 都为对象
1 | //Map 的 key 为对象属性,value 为对象,存在 key 重复的情况会报错:java.lang.IllegalStateException: Duplicate key |
Map 的 Key 重复处理策略,指定存在重复时取某一个元素
1 | //同上,指定 key 重复处理策略, 下例 key 重复取 key2 |
Collectors.toConcurrentMap:映射到ConcurrentMap
返回一个并发Collector ,该 Collector 将元素归积到 ConcurrentMap ,其 key
和 value
是将提供的映射函数应用于输入元素的结果。
如果存在 key
重复(根据Object.equals(Object) ),则在执行集合操作时会抛出 IllegalStateException 。 如果映射的键可能有重复项,请改用toConcurrentMap(Function, Function, BinaryOperator)
。
键 或 值 作为输入元素是很常见的。 在这种情况下,实用方法Function.identity()
可能会有所帮助。
例如,以下生成一个映射学生到他们的平均成绩的 Map :
1 | Map<Student, Double> studentToGPA = students.stream().collect(Collectors.toMap(Functions.identity(), student -> computeGPA(student))); |
下面生成一个映射到学生的唯一标识符的 Map :
1 | Map<String, Student> studentIdToStudent = students.stream().collect(Collectors.toConcurrentMap(Student::getId, Functions.identity())); |
Collectors.joining:连接元素
1 | // 转换元素并使用逗号将期拼接 |
Collectors.summingInt:计算总额
针对三种数据类型计算总额的方法:summingInt,summingLong,summingDouble
1 | // 计算员工工资总额 |
Collectors.groupingBy:分组
1 | // 员工根据部门分组 |
Collectors.partitioningBy:元素分区
1 | // 把学生成绩按是否通过来分区, passing and failing |
Collectors.minBy:取最小元素
1 | Dog dog = dogList.stream().collect(Collectors.minBy(Comparator.comparing(Dog::getAge))).get(); |
Collectors.maxBy:取最大元素
1 | Dog dog = dogList.stream().collect(Collectors.maxBy(Comparator.comparing(Dog::getAge))).get(); |
Collectors.counting:统计元数数量
返回一个接受 T 类型元素的 Collector ,该元素计算输入元素的数量。 如果不存在元素,则结果为 0。底层调的是 Long 的 sum 方法。
1 | Long count = dogList.stream().collect(Collectors.counting()); |
Collectors.averagingInt:求平均值
针对三种数据类型求平均值的方法:averagingInt,averagingLong,averagingDouble
1 | Double avg = dogList.stream().collect(Collectors.averagingInt(Dog::getAge)); |
Collectors.summarizingInt:获取汇总统计对象
针对三种数据类型获取汇总统计对象的方法:summarizingInt,summarizingLong,summarizingDouble
汇总统计对象,用于收集计数、最小值、最大值、总和和平均值等统计信息。
1 | IntSummaryStatistics statistics = dogList.stream().collect(Collectors.summarizingInt(Dog::getAge)); |
统计对象提供了方法获取 count, sum, min, max, average
等值。
此类旨在与(尽管不需要)流一起使用。此实现不是线程安全的。 但是,在并行流上使用 Collectors.toIntStatistics() 是安全的,因为Stream.collect()的并行实现为安全高效的并行执行提供了必要的分区、隔离和结果合并。
此实现不检查总和的溢出。
Collectors.mapping:映射转换
mapping()
收集器在用于多级归约时最有用,例如 groupingBy 或 partitioningBy 下游。
例如,给定一个Person流,要累积每个城市的姓氏集:
1 | Map<String, Set<String>> collect2 = dogList.stream().collect(Collectors.groupingBy(Dog::getColor, Collectors.mapping(Dog::getName, Collectors.toSet()))); |
forEach:遍历元素
此方法入参是一个消费函数接口,是流中的每一个元素执行操作。
对于并行流管道,此操作并不保证尊重流的顺序,因为这样做会牺牲并行的性能。 对于任何给定的元素,可以在库选择的任何时间和线程中执行该操作。
1 | strList.stream().forEach(System.out::println); |
forEachOrdered:顺序遍历元素
如果流具有定义的顺序,则按顺序对此流的每个元素执行操作,此操作一次处理一个元素。
1 | strList.stream().forEachOrdered(System.out::println); |
min:获取最小值元素
根据提供的 Comparator 返回此流的最小元素。方法入参是一个 Comparator 函数接口。
1 | String min = strList.stream().min(Comparator.comparing(String::length)).get(); |
max:获取最大值元素
根据提供的 Comparator 返回此流的最大元素。方法入参是一个 Comparator 函数接口。
1 | String max = strList.stream().max(Comparator.comparing(String::length)).get(); |
count:统计元素个数
1 | long count = Stream.of("Hello", "World", "China", "ShenZhen").count(); |
findFirst:查找第一个元素
1 | String first = Stream.of("Hello", "World", "China", "ShenZhen").findFirst().get(); |
findAny:随机查找一个元素
自由选择流中的任意一个元素,目的为了在并行操作中实现最大性能,代价是对同一源的多次调用可能不会返回相同的结果。
1 | String any = Stream.of("Hello", "World", "China", "ShenZhen").findAny().get(); |
allMatch:匹配所有
条件匹配所有元素,满足返回 true。方法是个入参断言函数。
1 | List<Dog> dogList = new ArrayList<>(); |
anyMatch:匹配其中一个
条件匹配任意一个元素,满足返回 true。
1 | // 条件匹配任意一个元素 |
noneMatch:全部不匹配
条件所有不匹配,有不满足返回 true。相当于 allMatch取 反。
1 | // 所有元素不匹配 |
Stream 应用示例
Collectors.toList
归集到一个新的 List。
1 | //归集到一个新的List |
Collectors.toMap集合转Map
传统的集合转 Map,是遍历每个元素对象,取元素对象来 put 进 map,而 Stream 的方式更便捷。
Map 的 Key 和 Value 都为对象属性
1
2//Map 的 key 和 value 都为对象属性
Map<Long, String> idNameMap = userList.stream().collect(Collectors.toMap(User::getId, UserVO::getName));Map 的 Key 为对象属性, Value 都为对象
1
2
3
4//Map 的 key 为对象属性,value 为对象,存在 key 重复的情况会报错:java.lang.IllegalStateException: Duplicate key
Map<Long, User> userIdMap2 = userList.stream().collect(Collectors.toMap(User::getId, e -> e));
// 或
Map<Long, User> userIdMap1 = userList.stream().collect(Collectors.toMap(User::getId, Function.identity()));Map 的 Key 重复处理策略,指定存在重复时取某一个元素
1
2
3
4//同上,指定 key 重复处理策略, 下例 key 重复取 key2
Map<Long, User> userIdMap3 = userList.stream().collect(Collectors.toMap(User::getId, p -> p, (key1, key2) -> key2));
//与上完全相同
Map<Long, User> userIdMap4 = userList.stream().collect(Collectors.toMap(User::getId, Function.identity(), (key1,key2) -> key2));
Collectors.groupingBy集合分组
相比传统的循环遍历比较再填值简洁太多了。
1 | Map<String, List<Dog>> collect = dogList.stream().collect(Collectors.groupingBy(Dog::getColor)); |
Collectors.partitioningBy集合分区
根据 Predicate (断言)对输入元素进行分区,并将它们组织成 Map<Boolean, List<T>>
。 对返回的Map的类型、可变性、可序列化性或线程安全性没有任何保证。
将是否满足断言的元素分为 true
和 false
两个组,如下示例:
1 | List<User> userList = new ArrayList<>(); |
结果:
1 | { |
Collectors.joining字符串连接
1 | String join = strList.stream().filter(str -> !str.isEmpty()).collect(Collectors.joining(",")); |
Stream map 映射
map()
生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。1
List<Integer> ageList = userList.stream().map(user -> user.getAge()).distinct().collect(Collectors.toList());
flatMap()
把 Stream 中的层级结构扁平化并返回 Stream。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*交集*/
List<AClass> intersectResult = userList1.stream().filter(userList2::contains).collect(Collectors.toList());
/*并集*/
List<AClass> unionResult = Stream.of(userList1, userList2).flatMap(Collection::stream).distinct().collect(Collectors.toList());
assertEquals(unionResult.size(), 5);
/*差集*/
List<AClass> differenceResult = userList1.stream().filter(x -> !userList2.contains(x)).collect(Collectors.toList());
map.put(1, new ListContainer(userList1));
map.put(2, new ListContainer(userList2));
/*合并多个list*/
List<AClass> aClassListResult = map.values().stream().flatMap(listContainer -> listContainer.getLst().stream()).collect(Collectors.toList());
/*注意跟并集的区别*/
assertEquals(aClassListResult.size(), 6);
其它参考
Java基础:JDK8 Stream 的方法应用