stream的实用方法和注意事项

简介: 相信大家一定都在项目开发中享受过stream带来的便利性和优雅的代码风格。接下来补充几个项目中不常见到但是同样实用的api,同时跟大家一起探讨stream这把双刃剑的另一面。

使用但不常见的方法


filter、map、skip等方法想必大家都十分熟悉 无需赘述。这里仅介绍工程中使用较少但同样实用的方法。


▐  reduce


reduce有3个参数:初始值、累加器、组合器。下面通过几个case为大家逐一讲解。由于比较绕,下面贴上ide执行结果

image.png

当顺序读流或者累加器的参数和它的实现的类型匹配时,我们不需要使用组合器。通常只有在处理对象属性时则需要组合器来帮助编译器推断入参类型。实际在串行流中组合器并不会实际执行,只需要出入参类型满足编译器推断要求即可。可以看到上方result3的计算,末尾组合器适用max还是min 结果是一样的。


▐  allMatch/anyMatch/noneMatch


判断集合中是否 全部都匹配/存在任意匹配/不存在匹配 某一规则。


比如下面一段代码,判断集合中的对象是否全部合法。语义十分简单。下面对比stream写法和常规写法。两种写法的运行结果是一样的。

@Data
@AllArgsConstructor
public static class Calendar {
    private LocalDate date;
    private boolean today;
    private boolean signed;
}
//日历初始化
LocalDate now = new LocalDate();
List<Calendar> calendars = Arrays.asList(
    new Calendar(new LocalDate(1661174238000L), false, false)
    , new Calendar(new LocalDate(1661828371000L), false, false)
    , new Calendar(new LocalDate(1661433438000L), false, false)
    , new Calendar(new LocalDate(1661519838000L), false, false)
    , new Calendar(new LocalDate(1661779038000L), false, false)
    , new Calendar(now, true, true)
);
//判断昨天是否签到过。写法一
boolean yesterdaySigned = calendars.stream()
    .anyMatch(
        t -> Days.daysBetween(t.getDate(), now).getDays() == 1 && t.isSigned()
    );
System.out.println("昨天是否签到过 -> " + yesterdaySigned);
//写法二
boolean yesterdaySigned2 = false;
for (Calendar calendar : calendars) {
    if (Days.daysBetween(calendar.getDate(), now).getDays() == 1) {
        //找到昨天的日历,并判断是否签到
        yesterdaySigned2 = calendar.isSigned();
        break;
    }
}
System.out.println("昨天是否签到过写法二 -> " + yesterdaySigned2);

这里写法一虽然更简练但是存在问题,大家有看出来的吗。这个问题放在“注意事项”中专门讲解。


▐  flatMap


跟map的区别是可以将一个对象转化成多个对象并以流的方式返回,适合用于集合嵌套场景下的扁平化处理。概念较为拗口,以下用ide截图演示。可以看到特定场景下flatmap相对map有先天优势。

image.png

注意事项


▐  书写顺序影响性能


stream实际使用中,filter和map最为常见。这两个操作都是逐个元素执行并逐个向下游操作传递,我们称之为“垂直操作”(补充:sorted是“水平操作”,即会截断后续运算直至自己将流中所有元素操作完成)。其中filter较为特殊,被其拦截后不会继续向下游传递。基于此原理,尽可能将filter前置往往可以大幅提高stream操作性能。如下所示:

image.png

image.png

一个长度为5的字符集,map-filter-foreach 顺序执行 则会有5次map、5次filter、1次foreach;


filter-map-foreach顺序执行,则会有5次filter、1次map、1次foreach执行。并且很容易推断filter过滤度越高性能差异就会越明显。


原理不少人可能会觉得简单易懂,但遗憾的是在大型项目中往往总能找到有此类性能缺陷的代码,诸如

   List<Long> awardId = timeFilterAwardConfigs.stream()
            .map(config -> config.getAwardId())
            .filter(awardId -> awardId > 0)
            .collect(Collectors.toList());

但在更复杂的场景下,也并非要求filter无脑提前于其他操作。比如下面这个例子

  //假设一份用户集
        List<User> userList = Arrays.asList(
            new User("张三", 22)
            , new User("李四", 21)
            , new User("王五", 19)
            , new User("赵六", 25)
        );
        //要输出这份集合中所有用户所就职的公司的年度营业额总和,要求公司所在地都在杭州市余杭区
        // 注意用户中可能有无业游民。不考虑就职公司重合或者一人就职多家公司的情况。
        //写法一
        int allCompanyTurnover1 = userList.stream()
            .map(user -> calculateAnnualTurnover(queryUserCompany(user)))
            .filter(Objects::nonNull)
            .reduce(0, Integer::sum);
        //写法二
        int allCompanyTurnover2 = userList.stream()
            .filter(user -> {
                Company company = queryUserCompany(user);
                return company != null && !"余杭".equals(company.getLocal());
            })
            .map(user -> calculateAnnualTurnover(queryUserCompany(user)))
            .reduce(0, Integer::sum);

写法一显然更符合直觉,写法二虽然filter提前过滤掉了一部分数据,但是queryUserCompany存在重复计算。所以此种情况下就需要综合 filter过滤度和queryUserCompany重复计算的开销进行权衡。如果filter过滤度足够高(比如余杭的公司很少)同时queryUserCompany 资源开销不大,那么写法二更优,反之写法一更优。


▐  并非适用所有场景


  • 性能上


这里就可以说回到刚才讲anyMatch时看到的那段代码

//判断昨天是否签到过。写法一
boolean yesterdaySigned = calendars.stream()
    .anyMatch(
        t -> Days.daysBetween(t.getDate(), now).getDays() == 1 && t.isSigned()
    );
System.out.println("昨天是否签到过 -> " + yesterdaySigned);
//写法二
boolean yesterdaySigned2 = false;
for (Calendar calendar : calendars) {
    if (Days.daysBetween(calendar.getDate(), now).getDays() == 1) {
        //找到昨天的日历,并判断是否签到
        yesterdaySigned2 = calendar.isSigned();
        break;
    }
}
System.out.println("昨天是否签到过写法二 -> " + yesterdaySigned2);

image.png

显然 anyMatch 会无条件遍历所有元素再返回,而直观的遍历写法往往不会犯这种错误,拿到结果后可以提前break。大家可能会想到先利用filter过滤获获取“昨天”的日历,然后再anymatch

boolean yesterdaySigned = calendars.stream()
            .filter(t -> Days.daysBetween(t.getDate(), now).getDays() == 1)
            .anyMatch(Calendar::isSigned);

但是很可惜,filter同样会完整遍历整个集合。事实上遍观所有stream方法似乎都没有办法很好的解决这个问题。也欢迎大家一起探讨。


  • 可阅读性


摘取了某业务中判断周期内签到次数的方法,采用stream和for循环常规写法

private int getCycleActionCount(Date start, Date end, List<ActionCalendar> calendar) {
        int count = 0;
        for (ActionCalendar calendarDay : calendar) {
            Date date = calendarDay.getDate();
            if (date.after(start) && date.before(end) && calendarDay.isComplete()) {
                //在周期内任意一天签到,签到次数自增。
                count++;
            }
        }
        return count;
    }
    private int getCycleActionCount2(Date start, Date end, List<ActionCalendar> calendar) {
        return Math.toIntExact(
            calendar.stream()
                .filter(
                    //统计周期内签到天数
                    t -> (
                      t.getDate().after(start) && t.getDate().before(end) && t.isComplete()
                    )
                ).count()
        );
    }

这样看两者之间 光从可阅读性上看并没有特别大的区分度。而即使熟练的stream 爱好者,相信写出一段stream代码后也会多看几眼确认性能、缩进是否达到最优。可见在某些场景下无论性能、可读性还是书写便利性都不占优,此时stream似乎就不是最优选择了。


总结


stream在多数场景下都能帮助我们更快的写出优美的代码,但是在更为复杂的场景下则需要对API之间的执行顺序、lambda表达式的使用、甚至此场景是否适用stream写法进行一定的思考,以避免出现性能或可读性的缺陷。


总的来看stream和直观的for遍历是互补而非替代关系,两者搭配,干活不累。


此外stream家族中还有个强大的种子选手“parallelStream”(并行流)没有介绍。他通常用在超大集合的处理中,日常工程中难寻使用场景,同时使用上比上面说到的串行流处理有更多的注意事项。这里暂不展开分享。


相关文章
|
1月前
|
人工智能 API 开发工具
Skills比MCP更重要?更省钱的多!Python大佬这观点老金测了一周终于懂了
加我进AI学习群,公众号右下角“联系方式”。文末有老金开源知识库·全免费。本文详解Claude Skills为何比MCP更轻量高效:极简配置、按需加载、省90% token,适合多数场景。MCP仍适用于复杂集成,但日常任务首选Skills。推荐先用SKILL.md解决,再考虑协议。附实测对比与配置建议,助你提升效率,节省精力。关注老金,一起玩转AI工具。
|
安全 Linux 数据安全/隐私保护
国内外四款强大的远控使用体验:ToDesk、向日葵、AnyDesk、Microsoft 远程桌面横向比较
国内外四款强大的远控使用体验:ToDesk、向日葵、AnyDesk、Microsoft 远程桌面横向比较
2600 0
|
JSON fastjson Java
fastjson:对象转化成json出现$ref
fastjson:对象转化成json出现$ref
|
7月前
|
人工智能 前端开发 jenkins
2025 API 开发管理工具 Apipost 与 Apifox 全维度对比
本文深入对比了 Apipost 与 Apifox 两款 API 开发管理工具在设计、调试、文档管理、Mock 服务、离线支持、AI 能力及 CI/CD 集成等方面的优劣,全面评估其适用场景,为研发测试团队提供选型参考。
504 5
|
9月前
|
存储 机器学习/深度学习 算法
KMP、Trie树 、AC自动机‌ ,三大算法实现 优雅 过滤 netty 敏感词
KMP、Trie树 、AC自动机‌ ,三大算法实现 优雅 过滤 netty 敏感词
KMP、Trie树 、AC自动机‌ ,三大算法实现 优雅 过滤 netty  敏感词
|
消息中间件 Java 中间件
MQ四兄弟:如何保证消息可靠性
本文介绍了RabbitMQ、RocketMQ、Kafka和Pulsar四种消息中间件的可靠性机制。这些中间件通过以下几种方式确保消息的可靠传输:1. 消息持久化,确保消息在重启后不会丢失;2. 确认机制,保证消息从生产者到消费者都被成功处理;3. 重试机制,处理失败后的重试;4. 死信队列,处理无法消费的消息。每种中间件的具体实现略有不同,但核心思想相似,都是从生产者、中间件本身和消费者三个角度来保障消息的可靠性。
679 0
|
机器学习/深度学习 人工智能 自动驾驶
【人工智能】图像识别:计算机视觉领域的识别与处理资源概览
在快速发展的科技时代,计算机视觉(Computer Vision, CV)作为人工智能的一个重要分支,正深刻改变着我们的生活与工作方式。图像识别作为计算机视觉的核心任务之一,旨在让机器能够理解和解释数字图像或视频中的内容,进而执行诸如目标检测、图像分类、场景理解等复杂任务。本文将深入探讨图像识别领域的关键技术、常用数据集、开源框架及工具资源,为从事或关注该领域的专业人士提供一份全面的指南。
666 2
了解一下,Java8 Stream的中间操作顺序
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 垂直执行:map和filter先看下面一个例子,找出流中"b"的字符串,并将其转化为大写,包含两个中间操作 map 和 filter 以及结束操作forEach。
了解一下,Java8 Stream的中间操作顺序
|
前端开发 Java 测试技术
【IDEA+通义灵码插件】实现属于你的大模型编程助手
【IDEA+通义灵码插件】实现属于你的大模型编程助手
2524 0