代码之家  ›  专栏  ›  技术社区  ›  MatMat

Java流分组和求多个字段

  •  8
  • MatMat  · 技术社区  · 7 年前

    我有一个傻瓜名单

    class Foo {
        private String category;
        private int amount;
        private int price;
    
        ... constructor, getters & setters
    }
    

    我想按类别分组,然后是金额和价格。

    结果将存储在地图中:

    Map<Foo, List<Foo>> map = new HashMap<>();
    

    关键是持有汇总金额和价格的foo,列表作为同一类别所有对象的值。

    到目前为止,我尝试了以下方法:

    Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));
    

    现在,我只需要用一个包含汇总金额和价格的foo对象替换字符串键。这就是我被困的地方。我似乎找不到任何方法来做这件事。

    5 回复  |  直到 7 年前
        1
  •  15
  •   Sweeper    7 年前

    有点难看,但应该管用:

    list.stream().collect(Collectors.groupingBy(Foo::getCategory))
        .entrySet().stream()
        .collect(Collectors.toMap(x -> {
            int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
            int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
            return new Foo(x.getKey(), sumAmount, sumPrice);
        }, Map.Entry::getValue));
    
        2
  •  4
  •   Hulk    7 年前

    我的变奏 Sweepers answer 使用还原收集器而不是流式处理两次来合计各个字段:

            Map<Foo, List<Foo>> map = fooList.stream()
                    .collect(Collectors.groupingBy(Foo::getCategory))
                    .entrySet().stream()
                    .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                                Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                                            l.getAmount() + r.getAmount(),
                                            l.getPrice() + r.getPrice())))
                                .get(), 
                                e -> e.getValue()));
    

    不是真的 更好的 尽管如此,因为它创造了许多短暂的 Foos .

    但是请注意 Foo 必须提供 hashCode -和 equals -只需要 category 考虑到结果 map 正确工作。这可能不是你想要的 一般来说。我想定义一个单独的 FooSummary 类以包含聚合数据。

        3
  •  3
  •   fps    7 年前

    如果您有一个特殊的、专门的构造函数和 hashCode equals 方法在 Foo 如下:

    public Foo(Foo that) { // not a copy constructor!!!
        this.category = that.category;
        this.amount = 0;
        this.price = 0;
    }
    
    public int hashCode() {
        return Objects.hashCode(category);
    }
    
    public boolean equals(Object another) {
       if (another == this) return true;
       if (!(another instanceof Foo)) return false;
       Foo that = (Foo) another;
       return Objects.equals(this.category, that.category);
    }
    

    这个 哈希码 等于 上面的实现允许您使用 作为地图中有意义的键(否则地图将被破坏)。

    现在,借助于 执行 amount price 同时,您可以通过两个步骤来完成所需的操作。首先是方法:

    public void aggregate(Foo that) {
        this.amount += that.amount;
        this.price += that.price;
    }
    

    现在,最终的解决方案是:

    Map<Foo, List<Foo>> result = fooList.stream().collect(
        Collectors.collectingAndThen(
            Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
            m -> { m.forEach((k, v) -> v.forEach(k::aggregate)); return m; }));
    

    编辑:缺少一些观察结果…

    一方面,此解决方案强制您使用 哈希码 等于 认为两个不同 如果实例属于同一个实例,则它们相同 category . 也许这是不需要的,或者您已经有了一个考虑更多或其他属性的实现。

    另一方面,使用 作为一个映射的键,用它的一个属性对实例进行分组是一个非常不常见的用例。我觉得最好用 类别 属性按类别分组并具有两个映射: Map<String, List<Foo>> 保持团队和 Map<String, Foo> 保持聚合 价格 数量 ,钥匙是 类别 在这两种情况下。

    除此之外,此解决方案还将在条目放入映射后改变映射的键。这很危险,因为这样会破坏地图。然而,这里我只改变了 也不参与 哈希码 也不 等于 的实现。我认为在这种情况下,这种风险是可以接受的,因为这一要求并不常见。

        4
  •  1
  •   dehasi    7 年前

    我建议您创建一个助手类,它将保存数量和价格

    final class Pair {
        final int amount;
        final int price;
    
        Pair(int amount, int price) {
            this.amount = amount;
            this.price = price;
        }
    }
    

    然后只需收集要映射的列表:

    List<Foo> list =//....;
    
    Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                        foo -> new Pair(foo.getAmount(), foo.getPrice()),
                        (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));
    
        5
  •  0
  •   Dakshinamurthy Karra    7 年前

    我接受了一个解决方案:)

    public static void main(String[] args) {
        List<Foo> foos = new ArrayList<>();
        foos.add(new Foo("A", 1, 10));
        foos.add(new Foo("A", 2, 10));
        foos.add(new Foo("A", 3, 10));
        foos.add(new Foo("B", 1, 10));
        foos.add(new Foo("C", 1, 10));
        foos.add(new Foo("C", 5, 10));
    
        List<Foo> summarized = new ArrayList<>();
        Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>() {
            @Override
            public Foo apply(Foo t) {
                Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
                Foo f;
                if (!fOpt.isPresent()) {
                    f = new Foo(t.getCategory(), 0, 0);
                    summarized.add(f);
                } else {
                    f = fOpt.get();
                }
                f.setAmount(f.getAmount() + t.getAmount());
                f.setPrice(f.getPrice() + t.getPrice());
                return f;
            }
        }));
        System.out.println(collect);
    }