Java 流:使用 groupingBy 而不是列表时如何映射到单个项目

Java stream: How to map to a single item when using groupingBy instead of to list

收到订单 class 喜欢:

@ToString
@AllArgsConstructor
@Getter
static class Order {
    long customerId;
    LocalDate orderDate;
}

和订单列表:

List<Order> orderList = List.of(new Order(1, LocalDate.of(2020,Month.APRIL,21)),
                                new Order(1, LocalDate.of(2021,Month.APRIL,21)),
                                new Order(1, LocalDate.of(2022,Month.APRIL,21)),
                                new Order(2, LocalDate.of(2020,Month.APRIL,21)),
                                new Order(2, LocalDate.of(2021,Month.APRIL,21)),
                                new Order(3, LocalDate.of(2020,Month.APRIL,21)),
                                new Order(3, LocalDate.of(2022,Month.APRIL,21)),
                                new Order(4, LocalDate.of(2020,Month.APRIL,21)));

我需要获取 customerId 的列表,其中最后一个 orderDate 超过 6 个月。所以对于上面的例子 [2,4]。我的想法是首先按 customerId 分组,第二个映射到最后一个 orderDate,第三个过滤 6 个月以上的映射。关于如何使用最近的 orderDate

映射到单个订单,我卡在了第二步

第一步

Map<Long, List<Order>> grouped =
        orderList.stream()
                .collect(Collectors.groupingBy(Order::getCustomerId));

第二步(卡在这里如何更改以上内容以仅获取一项作为值)

Map<Long, Order> grouped =
        orderList.stream()
                .collect(Collectors.groupingBy(Order::getCustomerId, ???));

甚至更好

Map<Long, LocalDate> grouped =
        orderList.stream()
                .collect(Collectors.groupingBy(Order::getCustomerId, ???));

我试过使用 Collectors.mapping()Collectors.reducing()Collectors.maxBy() 但是有很多编译错误。

您可以将 Collectors.toMapmergeFunction 一起用于第 2 步:

 Map<Long, LocalDate> latestOrderByCustomer = 
            orderList.stream()
                     .collect(Collectors.toMap(Order::customerId, 
                                                Order::orderDate, 
                                                (order1, order2) -> order1.isAfter(order2) ? order1 : order2));

使用 Collectors.toMap 收集器获得按客户 ID 列出的订单的映射。之后,您可以只过滤那些超过 6 个月的订单。

参见下面的实现:

import java.time.LocalDate;
import java.time.chrono.ChronoLocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Collections;
import java.util.Objects;
import java.util.Comparator;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
public static List<Long> getCustomerIdsOfOrdersOlderThanSixMonths(final List<Order> orderList) {
    return Optional.ofNullable(orderList)
            .orElse(Collections.emptyList())
            .stream()
            .filter(o -> Objects.nonNull(o) && Objects.nonNull(o.getOrderDate()))
            .collect(Collectors.toMap(
                  Order::getCustomerId,
                  Function.identity(),
                  BinaryOperator.maxBy(Comparator.comparing(Order::getOrderDate))))
            .values()
            .stream()
            .filter(o -> o.getOrderDate()
                  .plusMonths(6)
                  .isBefore(ChronoLocalDate.from(LocalDate.now())))
            .map(Order::getCustomerId)
            .collect(Collectors.toList());
    }
}
List<Long> customerIds = getCustomerIdsOfOrdersOlderThanSixMonths(orderList);
// [2, 4]

您可以将 groupingBy() 与下游 maxBy() 收集器一起使用,然后将结果过滤为仅超过六个月的日期:

import java.time.LocalDate;
import java.time.Month;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class Demo {
    private record Order(long customerId, LocalDate orderDate) {}

    public static void main(String[] args) {
        List<Order> orderList =
            List.of(new Order(1, LocalDate.of(2020,Month.APRIL,21)),
                    new Order(1, LocalDate.of(2021,Month.APRIL,21)),
                    new Order(1, LocalDate.of(2022,Month.APRIL,21)),
                    new Order(2, LocalDate.of(2020,Month.APRIL,21)),
                    new Order(2, LocalDate.of(2021,Month.APRIL,21)),
                    new Order(3, LocalDate.of(2020,Month.APRIL,21)),
                    new Order(3, LocalDate.of(2022,Month.APRIL,21)),
                    new Order(4, LocalDate.of(2020,Month.APRIL,21)));

        final LocalDate sixMonthsAgo = LocalDate.now().minusMonths(6);

        List<Order> mostRecentOrders =
            orderList.stream()
            .collect(Collectors.groupingBy(Order::customerId,
                                           Collectors.maxBy(Comparator.comparing(Order::orderDate))))
            .values().stream()
            .filter(opt -> opt.filter(o -> o.orderDate().isBefore(sixMonthsAgo)).isPresent())
            .map(Optional::orElseThrow)
            .collect(Collectors.toList());

        System.out.println(oldOrders);
    }
}

产出

[Order[customerId=2, orderDate=2021-04-21], Order[customerId=4, orderDate=2020-04-21]]

首先,您会得到一张客户 ID 地图和(由于 Collectors.maxBy() 的工作方式,包裹在 Optional 中)最近的日期订单。然后过滤掉最近日期在最近六个月内的条目。然后从 Optional 中提取剩余的订单,并将它们 return 放在 List 中。如果您只需要客户 ID,而不关心 Order 对象的其余部分,请适当修改最终的 map() 和 returned 类型。

这是执行此操作的另一种方法。使用 Collectors.partioningBy 分隔日期。设置几个变量以帮助在分区和打印过程中保持正常。

static boolean WITHIN_LAST_SIX_MONTHS = false;
static boolean BEFORE_SIX_MONTHS_AGO = true;
  • 根据订单是在 6 个月前发布 (true) 还是在过去 6 个月内发布 (false) 来划分订单。
  • 然后将订单映射到 CustomerId 和 return 作为 Set(无需重复)
  • 就是这样。
Map<Boolean, Set<Long>> result = orderList.stream()
                .collect(Collectors.partitioningBy(
                        order -> order.getOrderDate()
                                .isBefore(LocalDate.now()
                                        .minusMonths(6)),
                        Collectors.mapping(
                                Order::getCustomerId,
                                Collectors.toSet())));

System.out.println("BEFORE_SIX_MONTHS_AGO="+result.get(BEFORE_SIX_MONTHS_AGO));
System.out.println("WITHIN_LAST_SIX_MONTHS="+result.get(WITHIN_LAST_SIX_MONTHS));
        

打印

BEFORE_SIX_MONTHS_AGO=[1, 2, 3, 4]
WITHIN_LAST_SIX_MONTHS=[1, 3]

现在只需从六个月前发生的订单中删除最近订单的 ID。

result.get(BEFORE_SIX_MONTHS_AGO).removeAll(result.get(WITHIN_LAST_SIX_MONTHS));
System.out.println(result.get(BEFORE_SIX_MONTHS_AGO));

打印

[2, 4]

请注意,对于分区收集器,可以使用 collectingAndThen 直接从 stream 中 return 编辑最终集。整理器看起来像这样。

thismap -> {
    Set<Long> retSet = new HashSet<>(thismap.get(BEFORE_SIX_MONTHS_AGO));
    retSet.removeAll(thismap.get(WITHIN_LAST_SIX_MONTHS));
    return retSet;
}

但是我觉得这很忙,实际上使事情复杂化。