Java Streams-按两个条件求和的结果分组

时间:2019-08-13 15:05:21

标签: java java-8 java-stream

我有一个应按两个条件分组的订单清单。

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;
    }
}

问题:

有什么方法可以一次解决上述任务?

6 个答案:

答案 0 :(得分:4)

  

有什么方法可以一次解决上述任务?

这取决于您“在一个流中”的意思。您想执行一种还原操作,最好将其归纳为一系列还原的组合:

  • 按月对订单分组
  • 在每个月度组中,汇总每个客户的订单以产生总金额
  • 在每个月度的每个客户合计结果中,选择金额最大的结果(注意:对于关系而言,定义不明确)

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

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

答案 1 :(得分:4)

尝试使用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 :(得分:2)

它有一个嵌套的流,因此它不是一个流,它返回pip install mysql

Map<String, Optional<BuyerDetails>>

所以有3个步骤:

  • 按月 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)) ) ) ) 分组
  • 按客户名称分组并汇总总订单金额Collectors.groupingBy(Order::getOrderMonth,
  • 过滤中间结果并仅留出最大金额Collectors.groupingBy(Order::getCustomer, Collectors.summarizingLong( Order::getAmount))的客户

输出为

max(Comparator.comparingLong(BuyerDetails::getAmount))

我也很好奇是否可以在没有其他流的情况下完成这项工作。

答案 3 :(得分:2)

好的,我们开始!以下代码只需调用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 :(得分:1)

我的方法(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())
                        )
                );
    }

答案 5 :(得分:0)

这不能用单个流来完成,因为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()
       )
);