尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java Stream 流中toMap 实战总结

Java Stream 流中toMap 实战总结 一、基础常用 3 种 Stream 写法1.Collectors.toMap三参数重载Key 冲突兜底的标准写法应用场景批量查询实体列表后构造Map主键, 实体对象或Map主键, 衍生字符串用于后续循环回填视图对象。该场景在一对多关联查询中极为常见可有效解决循环查库的 N1 性能问题。两种典型写法对比写法模式Value 存储内容适用场景toMap(id, m - m, (a,b) - a)实体对象本身回填时需要读取实体中多个字段分别赋值toMap(id, 类名::转换方法, (a,b) - a)经过转换的衍生结果拼接字符串、格式化结果等回填时仅需单一展示字段直接使用最终值三参数语义拆解MapLong, Material materialMap materialList.stream() .collect(Collectors.toMap( Material::getMaterialId, // ① Key 提取器Map 的 Key 取自对象的哪个字段 Material::formatDisplayName, // ② Value 提取器Map 的 Value 存储什么内容 (exist, conflict) - exist // ③ Key 冲突合并策略出现重复 Key 时如何处理 ));③ 冲突策略的 3 种标准模式策略写法语义说明适用场景(a, b) - a保留先出现的值忽略后续重复项⭐ 默认推荐保证不抛出IllegalStateException数据以首条为准(a, b) - b用后出现的值覆盖已有值需要以最新数据为准的场景(a, b) - a , b将重复项的 Value 拼接合并同一 Key 下多条信息需要聚合展示实践边界两参数版本的toMap在遇到重复 Key 时直接抛出异常业务开发中不建议使用即使从业务逻辑上判断不会重复也应保留三参数写法以提升代码健壮性。2. 方法引用::的 4 种标准形式方法引用是 Lambda 表达式的语法简化本质是对接收参数并直接转发调用场景的缩写。写法形式分类等价 Lambda 表达典型示例类名::getXxx实例方法引用针对流中元素x - x.getXxx()Material::getMaterialId对象实例::方法名外部对象实例方法引用x - this.xxx(x)this::buildDisplayText类名::静态方法名静态方法引用x - 类名.xxx(x)ObjectUtil::isNotNull类名::new构造方法引用() - new 类名()User::new理解要点箭头左侧作为参数传入的对象属于谁就使用谁域下的方法。3. 恒等函数m - m输入即输出MapLong, Material map list.stream() .collect(Collectors.toMap(Material::getMaterialId, m - m, (a, b) - a));上述代码中m - m表示流中元素原样输出不做任何转换。等价标准写法Function.identity()语义完全一致。使用时机Map 的 Value 需要存储完整实体对象后续需要读取该实体的多个字段分别赋值。两种场景对比// 场景一Value 存储衍生字符串仅回填单一展示字段 MapLong, String displayMap list.stream() .collect(Collectors.toMap(Material::getMaterialId, Material::formatDisplayName, (a,b) - a)); vo.setDisplayText(displayMap.get(id)); // 直接使用转换结果 // 场景二Value 存储完整对象需要读取多个字段 MapLong, Material entityMap list.stream() .collect(Collectors.toMap(Material::getMaterialId, Function.identity(), (a,b) - a)); vo.setName(entityMap.get(id).getName()); // 多次读取不同字段 vo.setSpecification(entityMap.get(id).getSpec());二、进阶高频 5 个 Stream 知识点4.map()与flatMap()一对一转换与多层嵌套拍平核心区别方法作用输入输出数量关系map()元素转换A → B一个输入元素 → 一个输出元素flatMap()转换并拍平A →CollectionB再将集合展开为独立元素一个输入元素 → 零个、一个或多个输出元素典型应用场景多层嵌套列表展开ListLong materialIds List.of(1001L, 1002L, 1003L); // 不使用 flatMap结果为嵌套结构后续处理困难 ListListBomItem nestedResult materialIds.stream() .map(materialMapper::selectBomItemsByMaterialId) .collect(Collectors.toList()); // 使用 flatMap两层嵌套拍平为单层列表 ListBomItem flatResult materialIds.stream() .map(materialMapper::selectBomItemsByMaterialId) // StreamListBomItem .flatMap(Collection::stream) // StreamBomItem .collect(Collectors.toList());判断经验当普通for循环内部出现嵌套for循环且内层循环向外部集合add元素时该场景适合使用flatMap重构。5.Collectors.groupingBy()按字段分组的聚合操作无需手动编写for if map.put/get十余行代码groupingBy支持分组后二次聚合一行即可完成分组统计。ListBindingRecord records queryAllBindingRecords(); // ① 基础分组按关联 ID 分组Value 为该组的完整记录列表 MapLong, ListBindingRecord groupBySupplier records.stream() .collect(Collectors.groupingBy(BindingRecord::getSupplierId)); // ② 分组 映射每组仅收集某字段的集合 MapLong, SetLong supplierToMaterialIds records.stream() .collect(Collectors.groupingBy( BindingRecord::getSupplierId, Collectors.mapping(BindingRecord::getMaterialId, Collectors.toSet()) )); // ③ 分组 计数统计每组元素数量 MapLong, Long supplierToCount records.stream() .collect(Collectors.groupingBy( BindingRecord::getSupplierId, Collectors.counting() )); // ④ 分组 求和聚合计算每组数值字段总和 MapLong, BigDecimal supplierToTotalAmount quoteList.stream() .collect(Collectors.groupingBy( QuoteRecord::getSupplierId, Collectors.summingBigDecimal(QuoteRecord::getTotalAmount) ));6.findFirst()配合Optional规避集合首元素空指针直接调用list.get(0)在集合为空时会抛出IndexOutOfBoundsException结合 Stream 与 Optional 可以显式处理查不到数据的分支。// 场景一查不到返回 null由上层判断 Material material materialMapper.selectByName(keyword).stream() .findFirst() .orElse(null); // 场景二查不到抛出业务异常中断当前逻辑 Material material materialMapper.selectByName(keyword).stream() .findFirst() .orElseThrow(() - new ServiceException(未查询到匹配的物料信息)); // 场景三查到才执行后续逻辑否则静默跳过 materialMapper.selectByName(keyword).stream() .findFirst() .map(Material::getMaterialId) .ifPresent(this::processMaterialBinding);7. Stream 惰性求值机制中间操作不立即执行Stream API 的计算模型分为两类操作理解该机制有助于排查流中逻辑未执行类问题。操作类型执行时机典型方法返回值中间操作不立即执行仅做标记遇到终止操作时一并计算filter()、map()、flatMap()、peek()、distinct()、sorted()新的 Stream 对象终止操作立即触发全部计算计算完成后 Stream 对象失效collect()、forEach()、findFirst()、count()、anyMatch()非 Stream 类型结果常见误用示例// ❌ 缺少终止操作中间操作的 peek / filter 均不会实际执行 recordList.stream() .peek(r - log.info(当前记录ID: {}, r.getId())) .filter(r - r.getStatus() 1); // 运行后无日志输出也不产生过滤结果 // ✅ 必须以终止操作收尾才能触发计算 ListRecord validRecords recordList.stream() .peek(r - log.info(当前记录ID: {}, r.getId())) .filter(r - r.getStatus() 1) .collect(Collectors.toList());8.peek()与forEach()调试查看与遍历处理两者均可以遍历并访问流中元素但语义定位完全不同。方法操作类型设计用途Stream 用完后是否可继续peek()中间操作调试阶段打印日志、非侵入式查看元素内容✅ 可继续串联后续操作forEach()终止操作对元素执行最终业务处理含写操作❌ Stream 生命周期结束SetLong materialIds records.stream() .map(DeliveryRecord::getMaterialId) .peek(id - log.debug(处理物料ID: {}, id)) // 中途插入日志不影响后续流程 .filter(Objects::nonNull) .collect(Collectors.toSet());使用规范peek()仅用于调试观察不应在其中修改对象内部状态或执行写操作涉及字段修改、数据写入等操作应使用forEach()。三、Stream 开发 5 条实践原则以下原则覆盖绝大多数业务开发场景遵循可有效规避常见问题toMap必写三参数始终显式声明 Key 冲突策略不依赖业务层面的理论上不会重复避免偶发数据异常导致接口整体报错。Stream 不可复用调用终止操作后 Stream 对象即失效如需多次遍历应重新基于源集合调用.stream()创建新实例。禁止流内操作数据库不得在filter()/map()等流操作内部发起数据库查询否则将引发 N1 性能问题。正确流程为先收集全部关联 ID → 批量查询 → 使用 Map 关联回填。Lambda 内不修改外部局部变量Lambda 表达式引用的外部变量要求为final或等效 final如需累加计数等场景可使用AtomicInteger等线程安全容器或改写为普通for循环。小数据集不使用并行流数据量在万级以下时parallelStream()的线程切换开销通常大于并行收益且共享变量写操作在并行模式下存在线程安全风险问题排查成本较高。四、关联查询批量回填标准模板以下为一对多关联场景下的性能最优写法兼顾了批量查询、Map 加速查找、可读性与可调试性适合作为通用模板// Step 1从原始数据中提取全部关联 ID去重 过滤空值 SetLong materialIds records.stream() .map(BindingVo::getMaterialId) .filter(Objects::nonNull) .collect(Collectors.toSet()); // Step 2单次 SQL 批量查询全部关联数据构造 Map 索引核心消除 N1 查询 MapLong, Material materialMap materialMapper.selectByIds(materialIds).stream() .collect(Collectors.toMap( Material::getMaterialId, Function.identity(), (exist, ignore) - exist )); // Step 3普通 for 循环回填可读性与调试友好性优于 stream.peek 方案 for (BindingVo vo : records) { if (vo.getMaterialId() null) { continue; } Material material materialMap.get(vo.getMaterialId()); if (material ! null) { vo.setMaterialName(material.getMaterialName()); vo.setSpecModel(material.getSpecModel()); } }该模板的设计考量批量查询将 ID 收集后一次性selectByIds避免循环中单条查库Map 索引基于 HashMap 的 O(1) 查找替代双层嵌套循环的 O(n²)普通 for 循环回填相比stream().peek()断点调试更直观出错定位成本更低空值判断显式的空值分支处理避免隐藏 NPE
返回列表