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”(并行流)没有介绍。他通常用在超大集合的处理中,日常工程中难寻使用场景,同时使用上比上面说到的串行流处理有更多的注意事项。这里暂不展开分享。


相关文章
|
安全 Linux 数据安全/隐私保护
国内外四款强大的远控使用体验:ToDesk、向日葵、AnyDesk、Microsoft 远程桌面横向比较
国内外四款强大的远控使用体验:ToDesk、向日葵、AnyDesk、Microsoft 远程桌面横向比较
3267 0
|
Linux
修复io.minio.errors.ErrorResponseException: Access denied错误
修复io.minio.errors.ErrorResponseException: Access denied错误
2362 0
|
JSON fastjson Java
fastjson:对象转化成json出现$ref
fastjson:对象转化成json出现$ref
|
11月前
|
人工智能 前端开发 算法
大厂CIO独家分享:AI如何重塑开发者未来十年
在 AI 时代,若你还在紧盯代码量、执着于全栈工程师的招聘,或者仅凭技术贡献率来评判价值,执着于业务提效的比例而忽略产研价值,你很可能已经被所谓的“常识”困住了脚步。
5878 92
大厂CIO独家分享:AI如何重塑开发者未来十年
|
10月前
|
存储 自然语言处理 测试技术
一行代码,让 Elasticsearch 集群瞬间雪崩——5000W 数据压测下的性能避坑全攻略
本文深入剖析 Elasticsearch 中模糊查询的三大陷阱及性能优化方案。通过5000 万级数据量下做了高压测试,用真实数据复刻事故现场,助力开发者规避“查询雪崩”,为您的业务保驾护航。
2639 89
|
9月前
|
人工智能 API 开发工具
Skills比MCP更重要?更省钱的多!Python大佬这观点老金测了一周终于懂了
加我进AI学习群,公众号右下角“联系方式”。文末有老金开源知识库·全免费。本文详解Claude Skills为何比MCP更轻量高效:极简配置、按需加载、省90% token,适合多数场景。MCP仍适用于复杂集成,但日常任务首选Skills。推荐先用SKILL.md解决,再考虑协议。附实测对比与配置建议,助你提升效率,节省精力。关注老金,一起玩转AI工具。
|
安全 Java
java.security.InvalidKeyException: Illegal key size。
java.security.InvalidKeyException: Illegal key size。
838 0
|
存储 人工智能 运维
AI 网关代理 RAG 检索:Dify 轻松对接外部知识库的新实践
Higress AI 网关通过提供关键桥梁作用,支持 Dify 应用便捷对接业界成熟的 RAG 引擎。通过 AI 网关将 Dify 的高效编排能力与专业 RAG 引擎的检索效能结合,企业可在保留现有 Dify 应用资产的同时,有效规避其内置 RAG 的局限,显著提升知识驱动型 AI 应用的生产环境表现。
3893 152
|
存储 人工智能 自然语言处理
RAG 调优指南:Spring AI Alibaba 模块化 RAG 原理与使用
通过遵循以上最佳实践,可以构建一个高效、可靠的 RAG 系统,为用户提供准确和专业的回答。这些实践涵盖了从文档处理到系统配置的各个方面,能够帮助开发者构建更好的 RAG 应用。
7514 117
|
人工智能 安全 Dubbo
Spring AI 智能体通过 MCP 集成本地文件数据
MCP 作为一款开放协议,直接规范了应用程序如何向 LLM 提供上下文。MCP 就像是面向 AI 应用程序的 USB-C 端口,正如 USB-C 提供了一种将设备连接到各种外围设备和配件的标准化方式一样,MCP 提供了一个将 AI 模型连接到不同数据源和工具的标准化方法。
10531 128