【问题标题】:Java Streams - group by two criteria summing resultJava Streams - 按两个标准求和结果分组
【发布时间】:2019-08-15 16:29:58
【问题描述】:

我有一个订单列表,我应该按两个标准进行分组。

Order_Id| Customer |    Date    | Amount |
   1    | "Sam"    | 2019-03-21 | 100    |
   2    | "Nick"   | 2019-03-21 | 102    |
   3    | "Dan"    | 2019-03-21 | 300    |
   4    | "Sam"    | 2019-04-21 | 400    |
   5    | "Jenny"  | 2019-04-21 | 220    |
   6    | "Jenny"  | 2019-04-12 | 330    |

应该找到每个月按总金额计算的顶级买家,例如:

{
  MARCH: { customer='Dan', amount=300 }, 
  APRIL: { customer='Jenny', amount=550 }
}

我找到了一个解决方案:

public class Main {

    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order(1L, "Sam", LocalDate.of(2019, 3, 21), 100L),
                new Order(2L, "Nick", LocalDate.of(2019, 3, 21), 102L),
                new Order(3L, "Dan", LocalDate.of(2019, 3, 21), 300L),
                new Order(4L, "Sam", LocalDate.of(2019, 4, 21), 400L),
                new Order(5L, "Jenny", LocalDate.of(2019, 4, 21), 220L),
                new Order(6L, "Jenny", LocalDate.of(2019, 4, 12), 330L)
        );

        solution1(orders);
    } 

    private static void solution1(List<Order> orders) {
        final Map<Month, Map<String, Long>> buyersSummed = new HashMap<>();

        for (Order order : orders) {
            Map<String, Long> customerAmountMap = buyersSummed.computeIfAbsent(order.getOrderMonth(), mapping -> new HashMap<>());
            customerAmountMap.putIfAbsent(order.getCustomer(), 0L);
            Long customerAmount = customerAmountMap.get(order.getCustomer());
            customerAmountMap.put(order.getCustomer(), customerAmount + order.getAmount());
        }

        final Map<Month, BuyerDetails> topBuyers = buyersSummed.entrySet().stream()
                .collect(
                        toMap(Entry::getKey, customerAmountEntry -> customerAmountEntry.getValue().entrySet().stream()
                                .map(entry -> new BuyerDetails(entry.getKey(), entry.getValue()))
                                .max(Comparator.comparingLong(BuyerDetails::getAmount)).orElseThrow())
                );

        System.out.println(topBuyers);
    }

}

我使用的数据模型:

class BuyerDetails {
    String customer;
    Long amount;

    public BuyerDetails(String customer, Long amount) {
        this.customer = customer;
        this.amount = amount;
    }

    public String getCustomer() {
        return customer;
    }

    public Long getAmount() {
        return amount;
    }

}

class Order {

    Long id;
    String customer;
    LocalDate orderDate;
    Long amount;

    public Order(Long id, String customer, LocalDate orderDate, Long amount) {
        this.id = id;
        this.customer = customer;
        this.orderDate = orderDate;
        this.amount = amount;
    }

    public Long getId() {
        return id;
    }

    public String getCustomer() {
        return customer;
    }

    public LocalDate getOrderDate() {
        return orderDate;
    }

    public Month getOrderMonth() {
        return getOrderDate().getMonth();
    }

    public Long getAmount() {
        return amount;
    }
}

问题:

有什么办法可以一次性解决上述任务吗?

【问题讨论】:

  • 没办法,你需要两个reduce操作,第一个是按人计算当月的所有订单,第二个是通过分组来检测当月的顶级买家

标签: java java-8 java-stream


【解决方案1】:

尝试使用groupingBysummingLongcomparingLong,如下所示

Map<Month, BuyerDetails> topBuyers = orders.stream()
    .collect(Collectors.groupingBy(Order::getOrderMonth,
             Collectors.groupingBy(Order::getCustomer,
             Collectors.summingLong(Order::getAmount))))
    .entrySet().stream()
    .collect(Collectors.toMap(Map.Entry::getKey,
             order -> order.getValue().entrySet().stream()
            .max(Comparator.comparingLong(Map.Entry::getValue))
            .map(cust -> new BuyerDetails(cust.getKey(), cust.getValue())).get()));

输出

{
  "MARCH": { "customer": "Dan", "amount": 300 }, 
  "APRIL": { "customer": "Jenny", "amount": 550 }
}

【讨论】:

    【解决方案2】:

    有什么办法可以一次性解决上述任务吗?

    这取决于您所说的“在一个流中”是什么意思。您想要执行一个归约操作,该操作可能最能表征为一系列归约的组合:

    • 按月对订单进行分组
    • 在每个月组内,汇总每个客户的订单以产生总金额
    • 在每个月的每个客户汇总结果组中,选择数量最多的一个(注意:在平局的情况下定义不明确)

    从 Stream API 的角度来看,对流执行这些单独的缩减中的任何一个都是对该流的终端操作。您可以使用新的流处理结果,甚至在语法上将它们链接在一起,但是尽管这可能采用单个方法调用的 的语法形式,但它不会构成在单个流上发生的所有操作.

    您还可以创建单个 Collector(或其中的组件),以便通过收集输入元素的流直接获得结果,但 在内部,该收集器仍然需要通过内部创建和使用额外的流,或通过非流 API 执行相同的任务来执行单个缩减。如果您再次计算这些内部操作,不,它不会构成对单个流执行操作。 (但如果你不考虑那些内部缩减,那么是的,这一切都在一个流中完成。)

    【讨论】:

      【解决方案3】:

      好的,我们开始吧!以下代码将为您提供所需的内容,只需 1 次调用 stream()

      Map<Month, BuyerDetails> grouped = orders.stream().collect(
        Collectors.groupingBy(Order::getOrderMonth,
          Collectors.collectingAndThen(
            Collectors.groupingBy(Order::getCustomer,
              Collectors.summingLong(Order::getAmount)
            ),
            ((Function<Map<String,Long>, Map.Entry<String,Long>>) 
              map -> Collections.max(
                map.entrySet(), Comparator.comparingLong(Map.Entry::getValue)
              )
            ).andThen(
              e -> new BuyerDetails(e.getKey(),e.getValue())
            )
          )
        )
      );
      System.out.println(grouped);
      

      输出:

      {MARCH={ customer='Dan', amount=300 }, APRIL={ customer='Jenny', amount=550 }}

      现在,这有点笨拙,所以让我们逐行浏览它,看看发生了什么:

      Map<Month, BuyerDetails> grouped = orders.stream().collect(
      

      首先,我们流式传输我们拥有的订单,

        Collectors.groupingBy(Order::getOrderMonth,
      

      按月份分组,我们发现:

          Collectors.collectingAndThen(
            Collectors.groupingBy(Order::getCustomer,
      

      每个客户和

              Collectors.summingLong(Order::getAmount)
            ),
      

      他们一个月内的总订单数。

            ((Function<Map<String,Long>, Map.Entry<String,Long>>)
      

      (我们转换为Function,所以我们可以在我们定义的lambda函数上使用andThen之类的方法)

              map -> Collections.max(
                map.entrySet(), Comparator.comparingLong(Map.Entry::getValue)
              )
      

      对于每个月,我们找到最大订单金额的客户。

            ).andThen(
      

      那么,我们

              e -> new BuyerDetails(e.getKey(),e.getValue())
      

      为所述客户创建新的买家详细信息

            )
          )
        )
      );
      

      并收集所有 Month/BuyerDetail 对。

      System.out.println(grouped);
      

      最后,我们打印创建的数据结构。

      【讨论】:

        【解决方案4】:

        它有一个嵌套流,所以它不是一个流,它返回Map&lt;String, Optional&lt;BuyerDetails&gt;&gt;

        
        orders.stream()
                .collect(
                    Collectors.groupingBy(Order::getOrderMonth,
                        Collectors.collectingAndThen(
                                Collectors.groupingBy(
                                        Order::getCustomer,
                                        Collectors.summarizingLong(Order::getAmount)
                                ),
                                e -> e.entrySet()
                                        .stream()
                                        .map(entry -> new BuyerDetails(entry.getKey(), entry.getValue().getSum()))
                                        .max(Comparator.comparingLong(BuyerDetails::getAmount))
                        )
                    )
                )
        

        所以有 3 个步骤:

        • 按月分组Collectors.groupingBy(Order::getOrderMonth,
        • 按客户名称和总订单金额进行分组Collectors.groupingBy(Order::getCustomer, Collectors.summarizingLong( Order::getAmount))
        • 过滤中间结果,只留下最大数量的客户max(Comparator.comparingLong(BuyerDetails::getAmount))

        输出是

        {
          APRIL = Optional [ BuyerDetails { customer = 'Jenny', amount = 550 } ],
          MARCH = Optional [ BuyerDetails { customer = 'Dan', amount = 300 } ]
        }
        

        我也很好奇这是否可以在没有额外流的情况下完成。

        【讨论】:

          【解决方案5】:

          我的方法(3 个流):

          private static void solution1(List<Order> orders) {
                  final Map<Month, BuyerDetails> topBuyers = orders.stream().collect(
                          Collectors.groupingBy(order -> order.getCustomer() + "$" + order.getOrderDate().getYear() + "." +
                                          order.getOrderMonth(),
                                  Collectors.reducing((ord1, ord2) -> {
                                      ord1.setAmount(ord1.getAmount() + ord2.getAmount());
                                      return ord1;
                                  }))).values().stream()
                          .collect(Collectors.groupingBy(order -> order.get().getOrderMonth(),
                                  maxBy(Comparator.comparing(order -> order.get().getAmount())))).values().stream()
                          .collect(
                                  toMap((key) -> key.get().get().getOrderMonth(),
                                          key -> new BuyerDetails(key.get().get().getCustomer(), key.get().get().getAmount())
                                  )
                          );
              }
          

          【讨论】:

          • OP 要求一个流,但你有三个:P
          • 是的,不可能合二为一
          【解决方案6】:

          这不能用单个流来完成,因为summax 都是终端操作,它们不能应用于同一个流。 最好把它分成两个操作

          Map<Month, Map<String, Long>> sumsByMonth = orders.stream().collect(
                  Collectors.groupingBy(
                      Order::getOrderMonth,
                      Collectors.groupingBy(
                              Order::getCustomer,
                              Collectors.mapping(
                                      Order::getAmount,
                                      Collectors.reducing(0L, a -> a, (a1, a2) -> a1 + a2)
                              )
                      )
                  )
          );
          
          Map<Month, BuyerDetails> topBuyers = sumsByMonth.entrySet().stream().collect(
                  Collectors.toMap(
                          Map.Entry::getKey,
                          sums -> sums.getValue().entrySet().stream()
                                  .max(Comparator.comparingLong(Map.Entry::getValue))
                                  .map(e -> new BuyerDetails(e.getKey(), e.getValue()))
                                  .get()
                 )
          );
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2023-03-29
            • 2017-03-27
            • 2017-06-07
            • 2020-09-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多