如何在Java中收集分组JPA Tuple到Map时避免列表含null元素?
问题与优化方案
问题背景
现有JPQL语句:
select e as entity, c.type as config_type from Entity e left join Config c on e = c.entity where c.type = (select t from Type t where t.name = 'SHOP')
需求是将查询结果按Entity分组,转为Map<Entity, List<ConfigType>>,其中无对应配置的Entity需映射为空列表,而非包含null的列表。
当前实现代码:
public Map<Entity, List<ConfigType>> getGroupedEntityToConfigTypesMap() { return em.createNamedQuery(Entity.NQ_GET_ENTITY_TO_CONFIG_TYPE, Tuple.class) .getResultStream() .collect(Collectors.groupingBy( tuple -> ((Entity) tuple.get("entity")), Collectors.mapping(tuple -> ((ConfigType) tuple.get("config_type")), Collectors.collectingAndThen(Collectors.toList(), list -> list.stream() .filter(Objects::nonNull) .collect(Collectors.toUnmodifiableList()))) )); }
该实现需要二次遍历列表过滤null,效率有待优化,希望找到更高效优雅的实现方式。
优化方案
1. 先修正JPQL语句(核心前提)
当前JPQL的where条件会过滤掉所有没有匹配Config的Entity——因为left join后c为null时,c.type是null,无法满足c.type = (select t...)的条件,导致最终结果中不会包含无配置的Entity,自然也无法生成对应空列表的映射。
需要将过滤条件移到left join的on子句中,确保保留所有Entity:
select e as entity, c.type as config_type from Entity e left join Config c on e = c.entity and c.type = (select t from Type t where t.name = 'SHOP')
这样,无对应Config的Entity会被保留,对应的config_type为null。
2. 优化Collectors收集逻辑,避免二次遍历
方式一:使用Java 9+的Collectors.filtering
直接在收集流程中过滤null,无需额外遍历:
public Map<Entity, List<ConfigType>> getGroupedEntityToConfigTypesMap() { return em.createNamedQuery(Entity.NQ_GET_ENTITY_TO_CONFIG_TYPE, Tuple.class) .getResultStream() .collect(Collectors.groupingBy( tuple -> (Entity) tuple.get("entity"), Collectors.mapping( tuple -> (ConfigType) tuple.get("config_type"), Collectors.filtering(Objects::nonNull, Collectors.toUnmodifiableList()) ) )); }
Collectors.filtering会在映射后直接过滤掉null值,一步生成无null的不可变列表,无需二次处理。
方式二:自定义Collector(兼容Java 8)
如果项目仍使用Java 8,没有Collectors.filtering,可以自定义一个Collector来实现“映射+过滤+收集”的组合逻辑,避免二次遍历:
public static <T, U> Collector<T, ?, List<U>> mappingAndFiltering(Function<? super T, ? extends U> mapper, Predicate<? super U> predicate) { return Collector.of( ArrayList::new, (list, item) -> { U mapped = mapper.apply(item); if (predicate.test(mapped)) { list.add(mapped); } }, (left, right) -> { left.addAll(right); return left; }, Collections::unmodifiableList ); } // 使用自定义Collector public Map<Entity, List<ConfigType>> getGroupedEntityToConfigTypesMap() { return em.createNamedQuery(Entity.NQ_GET_ENTITY_TO_CONFIG_TYPE, Tuple.class) .getResultStream() .collect(Collectors.groupingBy( tuple -> (Entity) tuple.get("entity"), mappingAndFiltering( tuple -> (ConfigType) tuple.get("config_type"), Objects::nonNull ) )); }
自定义Collector在累加阶段直接完成映射和过滤,一次遍历即可生成目标列表,效率更高。
方式三:利用Optional简化处理(Java 8+)
在映射时用Optional包装,过滤空值后收集:
public Map<Entity, List<ConfigType>> getGroupedEntityToConfigTypesMap() { return em.createNamedQuery(Entity.NQ_GET_ENTITY_TO_CONFIG_TYPE, Tuple.class) .getResultStream() .collect(Collectors.groupingBy( tuple -> (Entity) tuple.get("entity"), Collectors.mapping( tuple -> Optional.ofNullable((ConfigType) tuple.get("config_type")), Collectors.collectingAndThen( Collectors.toList(), list -> list.stream() .flatMap(Optional::stream) .collect(Collectors.toUnmodifiableList()) ) ) )); }
这种方式代码更简洁,可读性较好,适合对代码简洁性要求高的场景。
内容的提问来源于stack exchange,提问作者Dawid
相关产品推荐
相关产品推荐

