Java 列表、分区和获取排序中的最后一项

mat*_*th5 1 java java-stream

我有一个产品交易列表。我想找到 中每个产品的最终(最大)productTransactionId 销售List<ProductTransaction>。因此,我按 ProductId 对其进行分区,并按 ProductTransactionId 进行排序。下面示例中的最终列表List<Integer> (2, 5, 9) 如何做到这一点?我正在尝试使用流和过滤器。

@Data
public class ProductTransaction {
    private int productTransactionId;
    private int productId;
    private Date saleDate;
    private BigDecimal amount;
}
Run Code Online (Sandbox Code Playgroud)
产品交易ID 产品编号 发售日期 数量
1 1 2019年3月2日 5
2 1 2019 年 4 月 1 日 9
3 2 2019 年 4 月 1 日 2
4 2 2019年8月21日 3
5 2 2019年8月21日 4
6 3 2019年10月1日 2
7 3 2019年10月3日 5
8 3 2019年10月3日 7
9 3 2019年10月3日 8

(请忽略SaleDate,仅按ProductTransactionId排序;表输入数据,不一定已排序

目前使用Java 8

试图:

当前的长解决方案(想要更清晰的速记,或者更快的性能)

Set<Long> finalProductTransactionIds = new HashSet<>();
    
Set<Long> distinctProductIds =  productTransactions.stream()
        .map(ProductTransaction::getProductid)
        .collect(Collectors.toSet());

for (Long productId: distinctProductIds) {
    Long productTransactionId = productTransactions.stream()
            .filter(x -> x.getProductId() == productId])
            .sorted(Comparator.comparing(ProductTransaction::getProductTransactionId)
            .reversed())
            .collect(Collectors.toList()).get(0).getProductTransactionId();
    finalProductTransactionIds.add(productTransactionId);
}
Run Code Online (Sandbox Code Playgroud)

kni*_*ttl 6

如果您不介意展开选项,您可以按产品 ID 进行分组,然后使用映射 + maxBy 下游收集器。这避免了必须收集到临时列表,因为仅保留最后一项(但为可选实例增加了最小的开销)。

final Map<Integer, Optional<Integer>> map = transactions.stream()
        .collect(
                Collectors.groupingBy(
                        ProductTransaction::getProductId,
                        Collectors.mapping(
                                ProductTransaction::getProductTransactionId,
                                Collectors.maxBy(Comparator.naturalOrder()))));

final Collection<Optional<Integer>> optionalMax = map.values();
final List<Optional<Integer>> max = optionalMax.stream()
        .filter(Optional::isPresent)
        .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

还可以使用toMap收集器的特殊重载来避免可选类型:

final Collection<Integer> maxTransactionIds = transactions.stream()
        .collect(
                Collectors.toMap(
                        ProductTransaction::getProductId,
                        ProductTransaction::getProductTransactionId,
                        BinaryOperator.maxBy(Comparator.naturalOrder())))
        .values();
Run Code Online (Sandbox Code Playgroud)

感谢Eritrean指出getProductId返回一个 int,因此我们可以BinaryOperator.maxBy(Comparator.naturalOrder)用更短的Math::max( Math#max(int,int)) 方法引用替换通常适用的方法,它将返回两个整数中较大的值:

final Collection<Integer> maxTransactionIds = transactions.stream()
        .collect(
                Collectors.toMap(
                        ProductTransaction::getProductId,
                        ProductTransaction::getProductTransactionId,
                        Math::max))
        .values();
Run Code Online (Sandbox Code Playgroud)

也许您不喜欢 Stream API。您可以使用常规循环和Map#merge函数来实现相同的最终结果。如果你仔细观察,合并调用甚至看起来像toMap收集器(为什么会这样,留给读者作为练习:))。

final Map<Integer, Integer> maxTxPerProduct = new HashMap<>();
for (final ProductTransaction transaction : transactions) {
    maxTxPerProduct.merge(
            transaction.getProductId(),
            transaction.getProductTransactionId(),
            Math::max);
}
final Collection<Integer> max = maxTxPerProduct.values();
Run Code Online (Sandbox Code Playgroud)

它绝对避免创建流和收集器对象(无论如何,这很少是问题)。