【问题标题】:Collect groupBy on deep property在深层属性上收集 groupBy
【发布时间】:2018-09-26 15:11:55
【问题描述】:
private Map<String, Set<Square>> populateZuloSquare(List<Square> squares) {
    if (squares == null || squares.isEmpty()) {
        return emptyMap();
    }

    Map<String, Set<Square>> res = new HashMap<>();

    squares.stream()
        .filter(square -> {
            if (square.getZuloCodes().isEmpty()) {
                LOG("Ignored {}", square.id);
                return false;
            }
            return true;
        })
        .forEach(square -> {
          square.getZuloCodes()
            .forEach(code -> {
                res.putIfAbsent(code, new HashSet<>());
                res.get(code).add(square);
            }));
        });

    return Collections.unmodifiableMap(res);
}

上面的代码接收一个 Squares 列表,这些 Squares 里面可能包含 ZuloCodes。输出应该是一个不可变的 Map zuloCode,并使用该 UniquePrefix 对所有正方形进行赋值。 如您所见,我无法找到删除辅助集合 res 并使代码易于阅读的方法,有没有办法将该集合分解为 [zuloCode, square] 然后 collect.groupBy ?另外,如果过滤器内部如此难以阅读,您将如何解决?

【问题讨论】:

    标签: java-8 java-stream collectors


    【解决方案1】:

    标准方法是在使用groupingBy 收集之前使用flatMap,但是由于每个元素都需要原始Square,因此您需要映射到同时包含Square 实例和zulo 代码的对象String.

    由于 Java 中还没有标准的对或元组类型,解决方法是使用 Map.Entry 实例,像这样

    private Map<String, Set<Square>> populateZuloSquare0(List<Square> squares) {
        if (squares == null || squares.isEmpty()) {
            return emptyMap();
        }
        return squares.stream()
            .filter(square -> logMismatch(square, !square.getZuloCodes().isEmpty()))
            .flatMap(square -> square.getZuloCodes().stream()
                .map(code -> new AbstractMap.SimpleEntry<>(code, square)))
            .collect(Collectors.collectingAndThen(
                Collectors.groupingBy(Map.Entry::getKey,
                    Collectors.mapping(Map.Entry::getValue, Collectors.toSet())),
                Collections::unmodifiableMap));
    }
    private static boolean logMismatch(Square square, boolean match) {
        if(!match) LOG("Ignored {}", square.id);
        return match;
    }
    

    另一种方法是使用自定义收集器来迭代键:

    private Map<String, Set<Square>> populateZuloSquare(List<Square> squares) {
        if (squares == null || squares.isEmpty()) {
            return emptyMap();
        }
        return squares.stream()
            .filter(square -> logMismatch(square, !square.getZuloCodes().isEmpty()))
            .collect(Collector.of(
                HashMap<String, Set<Square>>::new,
                (m,square) -> square.getZuloCodes()
                    .forEach(code -> m.computeIfAbsent(code, x -> new HashSet<>()).add(square)),
                (m1,m2) -> {
                    if(m1.isEmpty()) return m2;
                    m2.forEach((key,set) ->
                        m1.merge(key, set, (s1,s2) -> { s1.addAll(s2); return s1; }));
                    return m1;
                },
                Collections::unmodifiableMap)
            );
    }
    

    请注意,此自定义收集器可以视为以下循环代码的并行变体:

    private Map<String, Set<Square>> populateZuloSquare(List<Square> squares) {
        if (squares == null || squares.isEmpty()) {
            return emptyMap();
        }
        Map<String, Set<Square>> res = new HashMap<>();
        squares.forEach(square -> {
            if(square.getZuloCodes().isEmpty()) LOG("Ignored {}", square.id);
            else square.getZuloCodes().forEach(
                code -> res.computeIfAbsent(code, x -> new HashSet<>()).add(square));
        });
        return Collections.unmodifiableMap(res);
    }
    

    当您不需要代码具有并行能力时,现在看起来可能还不错……

    【讨论】:

      【解决方案2】:

      这个怎么样。你可以使用mapmerge 操作来完成这件事。我也更新了过滤器并简化了它。

      squares.stream().filter(s -> !s.getZuloCodes().isEmpty())
          .forEach(s -> s.getZuloCodes().stream().forEach(z -> res.merge(z, new HashSet<>(Arrays.asList(s)),
              (s1, s2) -> Stream.of(s1, s2).flatMap(Collection::stream).collect(Collectors.toSet()))));
      

      【讨论】:

      • 使用地图合并而不是 putIfAbsent 看起来效率更低,而且更不可读也仍然保留辅助地图
      • 您能否向我们展示您的基准测试结果或将我们指向任何说明这一点的网站。我从未见过文档中提到过这样的事情。从字面上看,这种方法更具声明性和不可变性
      • 没有基准,但可以说是 100 个正方形?您的场景创建 1000 个地图、100 个条目、100 个集合和 100 个 Arrays.asList(s),并在最佳和最坏情况下合并它们。 (您可以考虑使用 singleList 而不是 asList) - 在我的情况下:最坏的情况是没有重复项,我将创建 1 个地图、100 个条目、100 个集合,并执行 100 个 set.add。 100% 重复的最佳情况我将创建 1 个地图、1 个集合并进行 100 次添加……我相信我的平均表现更快,因为我的数据集很少出现最坏的情况,而你的可能更具声明性,我可能错了,虽然这只是一种粗略的感觉
      • 抱歉拼错了 1000 应该读为 100 ...从 1000 示例开始,但是当我用完字符时,每个字符都删除了一个 0,我想我错过了一个
      猜你喜欢
      • 2023-03-23
      • 2017-11-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-12
      • 1970-01-01
      • 2013-12-17
      • 2013-03-16
      相关资源
      最近更新 更多