栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java8 Stream 概述及日常使用

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java8 Stream 概述及日常使用

Java8 Stream 日常使用总结
一、概述

Stream是Java8提供的一种处理集合的强大工具,可以将Stream理解为一个处理集合数据的管道,在这个管道的各个部分,开发人员可以进行各种操作(如筛选、映射、查找和匹配、排序等),最终得到自己所需的数据。

1.1 无Stream vs Stream

以一个业务场景为例:设备模型中除了有产品型号这一固定属性分组,还有其他的用户自定的属性分组,现在需要获取某个模型中除产品型号之外的所有分组名称

List fields = 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后,代码可以这样写:

List fields = 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处理集合有以下三个优点:

  1. 声明式——更简介,更易读

    使用Stream处理集合数据时,处理逻辑易读,业务意图明显

  2. 可复合——更灵活

    Stream完全是一种组合式的操作,可以将各种操作进行任意组合,类似于数学中的高阶函数

  3. 可并行——性能更优

    提供ParallelStream,无需开发者写任何的多线程代码即可使用多核优势加速计算

二、Stream简单介绍

使用流一般包括三个必要步骤

  • 构建流
  • 组织中间操作链
  • 使用终端操作
2.1 构建流 2.1.1 从集合生成流

日常开发构建流的最常用方式就是从已有集合生成流,Java8中Collection类中增加了stream()和parallelStream()方法,可以直接将集合转换为串行流和并行流。

// list是一个已有的集合对象
list.stream();
list.parallelStream();
2.1.2 由值创建流

可以使用静态方法 Stream.of(),通过显式值创建一个流,它可以接受任意数量的参数。

Stream stream = Stream.of("Java", "8", "Strem");
2.1.3 由数组创建流

可以使用静态方法 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() ,它会返回一个由指定文件中的各行构成的字符串流。

Stream stream = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());
2.1.5 由函数生成流:创建无限流

Stream API 提供了两个静态方法来从函数生成流:Stream.iterate() 和 Stream.generate()。这两个操作可以创建所谓的无限流。由 iterate 和generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。

// 偶数无限流
Stream stream = Stream.iterate(0, n -> n + 2);
// 生成五个0到1之间的随机双精度数
Stream stream = Stream.generate(Math::random)
    .limit(5);
2.2 中间操作 2.2.1 概览

得到流对象以后,可以进行多个中间操作,中间操作会返回另一个流,使得多个操作可以连接起来形成一个流水线。

操作返回类型使用的类型/函数式接口函数描述符
filterStreamPredicateT -> boolean
distinctStream
mapStreamFunctionT -> R
flatMapStreamFunctionT -> Stream
sortedStreamComparator(T, T) -> int
skipStreamlong
limitStreamlong
takeWhile(Java 9,切片)StreamPredicateT -> boolean
dropWhile(Java 9,切片)StreamPredicateT -> boolean

表格字段说明:

  • 操作

    中间操作方法名称

  • 返回类型

    流的中间操作返回的还是流

  • 使用的类型/函数式接口

    中间操作接收的参数是何类型或是何函数式接口,如果是函数式接口,则可以使用Lambda表达式书写

  • 函数描述符

    函数描述符是函数式接口的抽象方法的签名,也即Lambda表达式的签名。例如,Predicate 接口中的方法签名是 boolean test(T t),则其Lambda表达式的签名即为 T -> boolean,也就是函数描述符,其中 -> 符号前面的T代表方法的入参,符号后面的 boolean代表方法的返回值。参照函数描述符,我们可以使用Java8引入的Lambda表达式简化我们的代码。

2.2.2 map 和 flatmap
  • map(Function)

    map 操作是将流中的元素进行再次加工形成一个新流。这在开发中很有用。比如我们有一个学生集合,我们需要从中提取学生的年龄以分析学生的年龄分布曲线。放在 Java 8 之前 我们要通过新建一个集合然后通过遍历学生集合来消费元素中的年龄属性。现在我们通过很简单的流式操作就完成了这个需求。

对应的代码为:

List ages=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 先提取属性放入一个比较小的流,然后再将所有的流合并为一个流,有一种 “聚沙成塔” 的感觉。

2.3 终端操作 2.3.1 概览

中间操作无论如何叠加,得到的依然是流,并不是我们需要的对象类型。使用终端操作,能将流转化为我们需要的类型,比如 List、Integer,甚至是 void,从而完成将流转换为我们所需的新集合或计算流中元素个数的功能。

终端操作会消耗流,产生一个最终结果。在使用终端操作操作一个流后,不能再次使用该流,也不能使用任何的中间操作产生的流,否则将抛出异常:java.lang.IllegalStateException: stream has already been operated upon or closed

操作返回类型使用的类型/函数式接口函数描述符
anyMatchbooleanPredicateT -> boolean
noneMatchbooleanPredicateT -> boolean
allMatchbooleanPredicateT -> boolean
findAnyOptional
findFirstOptional
forEachvoidConsumerT -> void
countlong
collectRCollector
reduceOptionalBinaryOperator(T, T) -> T
2.3.2 collect

collect是Stream中最强大的终端操作,collect有两种用法:

// 1、简易实现版本
 R collect(Supplier supplier,
                  BiConsumer accumulator,
                  BiConsumer combiner);

// 2、高级用法,复杂操作都封装到Collector接口中
  R collect(Collector collector);

日常开发中主要使用第二种用法,这里暂且把它称之为Collector高级调用

三、日常使用

日常业务功能开发过程中,遇到大量对集合的操作场景,Stream的使用很大程度上简化了代码,提高了代码的简洁性和可读性,同时提到了开发效率。下面列举几个实际使用场景。

3.1 排序 3.1.1 sorted( Comparator.comparingLong() )

需求:按照id(整型)进行排序

List> retMapList = new ObjectMapper().convertValue(data, List.class);
// 按照flow流程图中节点的先后顺序进行排序(以flow-service服务的node_instance表中的id为序)
List> nodeInstanceList = retMapList.stream().sorted(
    Comparator.comparingLong(map -> (int) map.get("id")))
    .collect(Collectors.toList());
3.1.2 sorted( Comparator.naturalOrder() )

需求:按照字符串顺序进行排序

Set noRepeatValues = new HashSet<>();        
// 将值进行一个排序
List orderNoRepeatValues = noRepeatValues.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList());
3.1.3 sorted( Comparator.comparing() )

需求:按照时间逆序排序

List res = new ArrayList<>(selectionHistorys.size());
// 按createOrModifyTime逆序排序
return res.stream()
    .sorted(Comparator.comparing(SelectionHistoryDTO::getCreateOrModifyTime).reversed())
    .collect(Collectors.toList());
3.1.4 sorted( Comparator )

需求:按照自定义条件(is_merge字段为true的排在前面)排序

Map> 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);
}
3.2 去重 3.2.1 distinct()
List allCkeys = ckeyDao.findAllByCateId(cateId);
// 获取产品型号之外的所有分组名称
List group = allCkeys.stream()
	.filter((c) -> !c.getDisplay().equals(ProductionNumberDisplay))
	.map((c) -> c.getAttributeGroup())
	.distinct().collect(Collectors.toList());
3.2.1 toSet()

需求:对map中的title字段去重并获取title

HashSet> categoryInfo;
Set categoryInfoTitle = categoryInfo.stream().map(map -> map.get("title")).collect(Collectors.toSet());
3.2.2 collectingAndThen() & TreeSet

需求:依据对象中的某个字段对列表去重,并获取每个对象的所有信息

// 项目管理中的所有设备
List proManAllDevices = deviceDao.findAllByProjectManagementId(projectManagementId);
// 对设备按照resuorceId去重
ArrayList remDupDevices = proManAllDevices.stream().collect(
    Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Device::getResourceId))), ArrayList::new)
);
3.3 字符串拼接
Set blankFields;
String blankFieldsStr = blankFields.stream().collect(Collectors.joining("、"));
3.4 flatMap()
  • 需求:从分组组织的模型字段中,获取所有的字段

    // groupCkeys是分组组织的模型字段,键值为组别名,value为这个组别下的所有字段
    Map> groupCkeys = ckeyService.getGroupCkeys(cateId);
    List ckeyArr = groupCkeys.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
    
  • 需求:获取所有节点的已选设备编号

    // SelectedAndRepeatedDeviceDTO
    public class SelectedAndRepeatedDeviceDTO {
        
        private List selectedDevice;
        
        private List repeatedDevice;
    }
    
    Map nodeAndDeviceId;
    
    Set allDevices = nodeAndDeviceId.entrySet().stream()
        .flatMap(item -> item.getValue().getSelectedDevice().stream())
        .collect(Collectors.toSet());
    
3.5 forEach()

需求:过滤出各个条件序号对应的产品型号 {条件序号(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 并行流
List> 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());
                            });
                });
3.7 其它使用
// 记录所有的组合和该组合涵盖的能力
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操作进行了介绍。并结合本人开发场景列出具体实践。有一定参考和借鉴作用。

参考资料
  1. 《Java实战(第2版)》

  2. Java 8 Stream Api 中的 map和 flatMap 操作:https://juejin.cn/post/6844904000874086414

  3. Java8之Stream-强大的collect操作:https://juejin.cn/post/6844903497666674702

我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号