一、概述
Stream是Java8提供的一种处理集合的强大工具,可以将Stream理解为一个处理集合数据的管道,在这个管道的各个部分,开发人员可以进行各种操作(如筛选、映射、查找和匹配、排序等),最终得到自己所需的数据。
1.1 无Stream vs Stream以一个业务场景为例:设备模型中除了有产品型号这一固定属性分组,还有其他的用户自定的属性分组,现在需要获取某个模型中除产品型号之外的所有分组名称
Listfields = fieldDao.findByModelId(modeldId); Set fieldsSet = new HashSet<>(); for (Field field : fields) { // 1、获取不为产品型号的所有属性 if (!"产品型号".equals(field.getDisplay())) { // 2、获取属性分组并将其加入到Set中去重 fieldsSet.add(field.getAttributeGroup()); } } // 3、将Set转换为List List result = new ArrayList<>(fieldsSet);
因为需求相对简单,Stream之前的处理集合的方式看起来不算十分复杂,但是这段代码最大的问题在于可读性,阅读者需要仔细研读代码,才能意会到这段代码到底在做什么。
而使用Stream后,代码可以这样写:
Listfields = fieldDao.findByModelId(modeldId); List group = fields.stream() .filter(item -> !"产品型号".equals(field.getDisplay())) // 过滤得到非"产品型号"的属性 .map(item -> c.getAttributeGroup()) // 映射属性的分组信息 .distinct() // 去重 .collect(Collectors.toList());
使用Stream后,代码更加简洁,并且代码可读性大大提高,这对于后期的维护是很有帮助的
1.2 Stream优点概括起来,使用Stream处理集合有以下三个优点:
-
声明式——更简介,更易读
使用Stream处理集合数据时,处理逻辑易读,业务意图明显
-
可复合——更灵活
Stream完全是一种组合式的操作,可以将各种操作进行任意组合,类似于数学中的高阶函数
-
可并行——性能更优
提供ParallelStream,无需开发者写任何的多线程代码即可使用多核优势加速计算
使用流一般包括三个必要步骤
2.1 构建流 2.1.1 从集合生成流
- 构建流
- 组织中间操作链
- 使用终端操作
日常开发构建流的最常用方式就是从已有集合生成流,Java8中Collection类中增加了stream()和parallelStream()方法,可以直接将集合转换为串行流和并行流。
// list是一个已有的集合对象 list.stream(); list.parallelStream();2.1.2 由值创建流
可以使用静态方法 Stream.of(),通过显式值创建一个流,它可以接受任意数量的参数。
Stream2.1.3 由数组创建流stream = Stream.of("Java", "8", "Strem");
可以使用静态方法 Arrays.stream() 从数组创建一个流,它接受一个数组作为参数。
String[] numbers = {"1", "2", "3", "4", "5"};
Stream stream = Arrays.stream(numbers);
2.1.4 由文件生成流
Java中用于处理文件等I/O操作的 NIO API 已更新,以便利用 Stream API。java.nio.file.Files 中的很多静态方法都会返回一个流。例如,一个很有用的方法是 Files.lines() ,它会返回一个由指定文件中的各行构成的字符串流。
Stream2.1.5 由函数生成流:创建无限流stream = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());
Stream API 提供了两个静态方法来从函数生成流:Stream.iterate() 和 Stream.generate()。这两个操作可以创建所谓的无限流。由 iterate 和generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。
// 偶数无限流 Streamstream = Stream.iterate(0, n -> n + 2);
// 生成五个0到1之间的随机双精度数 Stream2.2 中间操作 2.2.1 概览stream = Stream.generate(Math::random) .limit(5);
得到流对象以后,可以进行多个中间操作,中间操作会返回另一个流,使得多个操作可以连接起来形成一个流水线。
| 操作 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
|---|---|---|---|
| filter | Stream | Predicate | T -> boolean |
| distinct | Stream | ||
| map | Stream | Function | T -> R |
| flatMap | Stream | Function | T -> Stream |
| sorted | Stream | Comparator | (T, T) -> int |
| skip | Stream | long | |
| limit | Stream | long | |
| takeWhile(Java 9,切片) | Stream | Predicate | T -> boolean |
| dropWhile(Java 9,切片) | Stream | Predicate | T -> boolean |
表格字段说明:
-
操作
中间操作方法名称
-
返回类型
流的中间操作返回的还是流
-
使用的类型/函数式接口
中间操作接收的参数是何类型或是何函数式接口,如果是函数式接口,则可以使用Lambda表达式书写
-
函数描述符
函数描述符是函数式接口的抽象方法的签名,也即Lambda表达式的签名。例如,Predicate 接口中的方法签名是 boolean test(T t),则其Lambda表达式的签名即为 T -> boolean,也就是函数描述符,其中 -> 符号前面的T代表方法的入参,符号后面的 boolean代表方法的返回值。参照函数描述符,我们可以使用Java8引入的Lambda表达式简化我们的代码。
-
map(Function
) map 操作是将流中的元素进行再次加工形成一个新流。这在开发中很有用。比如我们有一个学生集合,我们需要从中提取学生的年龄以分析学生的年龄分布曲线。放在 Java 8 之前 我们要通过新建一个集合然后通过遍历学生集合来消费元素中的年龄属性。现在我们通过很简单的流式操作就完成了这个需求。
对应的代码为:
Listages=studentList.stream().map(Student::getAge).collect(Collectors.toList());
-
flatMap(Function
>) 如果把上面的例子改一下,学生信息是以班级为单位组织的,gradeList 的类型是 List
- >,这时要提取所有班级下,所有学生的年龄,再使用上面的 map() 方法就行不通了
List
- > studentGroup = gradeList.stream().map(Grade::getStudents).collect(Collectors.toList());
通过上面的操作,我们只能得到每个班的学生集合的集合 List>。 我们还需要嵌套循环才能获取学生的年龄数据,十分不便。如果我们能返回全部学生的集合 List 就方便多了。flatMap 可以解决这个问题!
// flatMap 提取 List
map 提取年龄 List ages = gradeList.stream() .flatMap(grade -> grade.getStudents().stream()) .map(Student::getAge) .collect(Collectors.toList()); 如上述代码所示,使用 flatMap 将所有的学生汇聚到一起。然后再使用 map 操作提取年龄。 flatMap 不同于 map 地方在于 map 只是提取属性放入流中,而 flatMap 先提取属性放入一个比较小的流,然后再将所有的流合并为一个流,有一种 “聚沙成塔” 的感觉。
中间操作无论如何叠加,得到的依然是流,并不是我们需要的对象类型。使用终端操作,能将流转化为我们需要的类型,比如 List、Integer,甚至是 void,从而完成将流转换为我们所需的新集合或计算流中元素个数的功能。
终端操作会消耗流,产生一个最终结果。在使用终端操作操作一个流后,不能再次使用该流,也不能使用任何的中间操作产生的流,否则将抛出异常:java.lang.IllegalStateException: stream has already been operated upon or closed
| 操作 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
|---|---|---|---|
| anyMatch | boolean | Predicate | T -> boolean |
| noneMatch | boolean | Predicate | T -> boolean |
| allMatch | boolean | Predicate | T -> boolean |
| findAny | Optional | ||
| findFirst | Optional | ||
| forEach | void | Consumer | T -> void |
| count | long | ||
| collect | R | Collector | |
| reduce | Optional | BinaryOperator | (T, T) -> T |
collect是Stream中最强大的终端操作,collect有两种用法:
// 1、简易实现版本R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner); // 2、高级用法,复杂操作都封装到Collector接口中 R collect(Collector super T, A, R> collector);
日常开发中主要使用第二种用法,这里暂且把它称之为Collector高级调用
-
collector高级调用
Collector接口是使得collect操作强大的终极武器,对于绝大部分操作可以分解为其下主要步骤,提供初始容器->加入元素到容器->并发下多容器聚合->对聚合后结果进行操作,同时Collector接口又提供了of静态方法使开发人员最大化的定制自己的操作,官方也提供了Collectors这个类封装了大部分的常用收集操作。
另外CollectorImpl为Collector的实现类,因为接口不可实例化,这里主要完成实例化操作。//初始容器 Supplier supplier(); //加入到容器操作 BiConsumer accumulator(); //多容器聚合操作 BinaryOperator combiner(); //聚合后的结果操作 Function finisher(); //操作中便于优化的状态字段 Set
characteristics(); -
Collectors的方法封装
Collectors作为官方提供的收集工具类,其很多操作都具有参考性质,能帮助我们更加理解Collector接口,万变不离其宗,最终只是上面五个函数接口的混合操作,下面来分析下官方是如何使用这几个接口的。
-
toList()
容器: ArrayList::new
加入容器操作: List::add
多容器合并: left.addAll(right); return left;
聚合后的结果操作: 这里直接返回,因此无该操作,默认为castingIdentity()
优化操作状态字段: CH_ID
对于Map,Set等操作都是类似的实现。public static
Collector > toList() { return new CollectorImpl<>((Supplier - >) ArrayList::new,
List::add,
(left, right) -> {
left.addAll(right);
return left;
},
CH_ID);
}
- joining()
容器: StringBuilder::new
加入容器操作: StringBuilder::append
多容器合并: r1.append(r2); return r1;
聚合后的结果操作: StringBuilder::toString
优化操作状态字段: CH_NOIDpublic static Collector
joining() { return new CollectorImpl ( StringBuilder::new, StringBuilder::append, (r1, r2) -> { r1.append(r2); return r1; }, StringBuilder::toString, CH_NOID); } - groupingBy()
groupingBy是toMap的一种高级方式,弥补了toMap对值无法提供多元化的收集操作,比如对于返回Map
>这样的形式toMap就不是那么顺手,那么groupingBy的重点就是对Key和Value值的处理封装。分析如下代码,其中classifier是对key值的处理,mapFactory则是指定Map的容器具体类型,downstream为对Value的收集操作。 public static
> Collector groupingBy(Function super T, ? extends K> classifier, Supplier mapFactory, Collector super T, A, D> downstream) { } 对于之前用原生collect方法做的收集操作那么就可以很容易改写为groupBy形式
//原生形式 Lists.
newArrayList().stream() .collect(() -> new HashMap >(), (h, x) -> { List value = h.getOrDefault(x.getType(), Lists.newArrayList()); value.add(x); h.put(x.getType(), value); }, HashMap::putAll ); //groupBy形式 Lists. newArrayList().stream() .collect(Collectors.groupingBy(Person::getType, HashMap::new, Collectors.toList())); //因为对值有了操作,因此我可以更加灵活的对值进行转换 Lists. newArrayList().stream() .collect(Collectors.groupingBy(Person::getType, HashMap::new, Collectors.mapping(Person::getName, Collectors.toSet()))); -
reducing()
reducing是针对单个值的收集,其返回结果不是集合家族的类型,而是单一的实体类T
容器: boxSupplier(identity),这里包裹用的是一个长度为1的Object[]数组,至于原因自然是不可变类型的锅
加入容器操作: a[0] = op.apply(a[0], t)
多容器合并: a[0] = op.apply(a[0], b[0]); return a;
聚合后的结果操作:结果自然是Object[0]所包裹的数据a -> a[0]
优化操作状态字段: CH_NOIDpublic static
Collector reducing(T identity, BinaryOperator op) { return new CollectorImpl<>( boxSupplier(identity), (a, t) -> { a[0] = op.apply(a[0], t); }, (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; }, a -> a[0], CH_NOID); } 那么接下来就是对之前Collect的一些操作的改造
//原生操作 final Integer[] integers = Lists.newArrayList(1, 2, 3, 4, 5) .stream() .collect(() -> new Integer[]{0}, (a, x) -> a[0] += x, (a1, a2) -> a1[0] += a2[0]); //reducing操作 final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5) .stream() .collect(Collectors.reducing(0, Integer::sum)); //当然Stream也提供了reduce操作 final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5) .stream().reduce(0, Integer::sum)
-
日常业务功能开发过程中,遇到大量对集合的操作场景,Stream的使用很大程度上简化了代码,提高了代码的简洁性和可读性,同时提到了开发效率。下面列举几个实际使用场景。
3.1 排序 3.1.1 sorted( Comparator.comparingLong() )需求:按照id(整型)进行排序
List3.1.2 sorted( Comparator.naturalOrder() )
需求:按照字符串顺序进行排序
Set3.1.3 sorted( Comparator.comparing() )noRepeatValues = new HashSet<>(); // 将值进行一个排序 List orderNoRepeatValues = noRepeatValues.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList());
需求:按照时间逆序排序
List3.1.4 sorted( Comparator )res = new ArrayList<>(selectionHistorys.size()); // 按createOrModifyTime逆序排序 return res.stream() .sorted(Comparator.comparing(SelectionHistoryDTO::getCreateOrModifyTime).reversed()) .collect(Collectors.toList());
需求:按照自定义条件(is_merge字段为true的排在前面)排序
Map3.2 去重 3.2.1 distinct()> result = new HashMap<>(group.size()); // 在每个分组下,将需要合并(is_merge为true)的字段属性排在前面 for(Map.Entry > entry: result.entrySet()){ List sortedCkeyList = entry.getValue().stream().sorted((o1, o2) -> { if (o1.getIsMerge() == true && o2.getIsMerge() == false) { return -1; // 降序排列,is_merge为true的排在前面 } else if (o1.getIsMerge() == false && o2.getIsMerge() == true) { return 1; } else { return 0; } }).collect(Collectors.toList()); result.put(entry.getKey(), sortedCkeyList); }
List3.2.1 toSet()allCkeys = ckeyDao.findAllByCateId(cateId); // 获取产品型号之外的所有分组名称 List group = allCkeys.stream() .filter((c) -> !c.getDisplay().equals(ProductionNumberDisplay)) .map((c) -> c.getAttributeGroup()) .distinct().collect(Collectors.toList());
需求:对map中的title字段去重并获取title
HashSet3.2.2 collectingAndThen() & TreeSet> categoryInfo; Set categoryInfoTitle = categoryInfo.stream().map(map -> map.get("title")).collect(Collectors.toSet());
需求:依据对象中的某个字段对列表去重,并获取每个对象的所有信息
// 项目管理中的所有设备 List3.3 字符串拼接proManAllDevices = deviceDao.findAllByProjectManagementId(projectManagementId); // 对设备按照resuorceId去重 ArrayList remDupDevices = proManAllDevices.stream().collect( Collectors.collectingAndThen( Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Device::getResourceId))), ArrayList::new) );
Set3.4 flatMap()blankFields; String blankFieldsStr = blankFields.stream().collect(Collectors.joining("、"));
-
需求:从分组组织的模型字段中,获取所有的字段
// groupCkeys是分组组织的模型字段,键值为组别名,value为这个组别下的所有字段 Map
> groupCkeys = ckeyService.getGroupCkeys(cateId); List ckeyArr = groupCkeys.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); -
需求:获取所有节点的已选设备编号
// SelectedAndRepeatedDeviceDTO public class SelectedAndRepeatedDeviceDTO { private ListselectedDevice; private List repeatedDevice; } Map nodeAndDeviceId; Set allDevices = nodeAndDeviceId.entrySet().stream() .flatMap(item -> item.getValue().getSelectedDevice().stream()) .collect(Collectors.toSet());
需求:过滤出各个条件序号对应的产品型号 {条件序号(Integer): {产品型号1,产品型号2}(Set)}
// {条件序号: {产品型号1,产品型号2}
Map> indexProductionMap = new HashMap<>(16);
for(Map.Entry> map: indexProductionMap.entrySet()){
Set productSet = map.getValue();
productSet.stream()
.forEach(product -> productionAbility.computeIfAbsent(product, p -> new HashSet<>())
.add(map.getKey()));
}
3.6 并行流
List3.7 其它使用> conditionMapList; conditionMapList.parallelStream() .forEach(conditionMap -> { conditionMap.entrySet().stream() .forEach(entry -> { String key = entry.getKey(); String value = entry.getValue(); products.stream() .filter(product -> product.getValue().stream() // 该行的map对应上面List >中的每个map,对应数据库Product表中value字段的某个map,例:{"NvavonLNwEFmHgCnAIdentity": "a91212c5-5060-4b1d-a949-39c8c5b83332", "Sensor": "IMX307"} .anyMatch(map -> map.entrySet().stream() .anyMatch(tempEntry -> tempEntry.getKey().equals(key) && tempEntry.getValue().contains(value)))) .map(product -> indexProductionMap.computeIfAbsent(condition_index_map.get(key+ ":" + value), p -> new CopyOnWriteArraySet<>()) // 若indexProductionMap中不存在当前条件序号为键的map,则新建一个HashSet,将product中的production字段按","分隔,放入到Set中;否则取出条件序号为键的Set,将当前product的production字段按","分隔,添加到原来的Set中 .addAll(Arrays.asList(product.getProduction().split(",")))) .collect(Collectors.toList()); }); });
// 记录所有的组合和该组合涵盖的能力 Map, Set
> planList = new LinkedHashMap<>(); // 获取最多满足多少个条件 Optional > maxConditionNumSet = planList.entrySet().stream().map(map -> map.getValue()).max(Comparator.comparingInt(Set::size));
private void combine(int index, int proNum, String[] productionArr, List四、总结> combinationResult, List
tempArr) { if(proNum == 1){ for(int i = index; i < productionArr.length; i++){ tempArr.add(productionArr[i]); // 不能写成 combinationResult.add(tempArr),因为是浅拷贝,下一步的remove操作会清空这一步的操作 combinationResult.add(tempArr.stream().collect(Collectors.toList())); tempArr.remove(productionArr[i]); } } else if(proNum > 1){ for (int i = index; i < productionArr.length; i++){ tempArr.add(productionArr[i]); combine(i + 1, proNum - 1, productionArr, combinationResult, tempArr); tempArr.remove(productionArr[i]); } } }
本案例介绍了使用流处理集合数据的优点,概述了流的使用步骤:构建流->中间操作->终端操作,对常用的api操作进行了介绍。并结合本人开发场景列出具体实践。有一定参考和借鉴作用。
参考资料-
《Java实战(第2版)》
-
Java 8 Stream Api 中的 map和 flatMap 操作:https://juejin.cn/post/6844904000874086414
-
Java8之Stream-强大的collect操作:https://juejin.cn/post/6844903497666674702



