繁体   English   中英

Java Stream GroupBy 和 Reduce

[英]Java Stream GroupBy and Reduce

我有一个项目 class,其中包含一个代码、数量和金额字段,以及一个可能包含许多项目(具有相同代码)的项目列表。 我想按代码对项目进行分组并总结它们的数量和金额。

我能够使用流的groupingByreduce实现一半。 分组方式有效,但减少是将所有分组项目减少为一个在不同代码上重复的单个项目( groupingBy键)。

此处不应该减少从 map 中减少每个代码的项目列表吗? 为什么要为所有人重新调整相同的组合项目。

下面是一个示例代码。

import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.Map;

class HelloWorld {
    public static void main(String[] args) {
        List<Item> itemList = Arrays.asList(
            createItem("CODE1", 1, 12),
            createItem("CODE2", 4, 22),
            createItem("CODE3", 5, 50),
            createItem("CODE4", 2, 11),
            createItem("CODE4", 8, 20),
            createItem("CODE2", 1, 42)
        );
        
        Map<String, Item> aggregatedItems = itemList
            .stream()
            .collect(Collectors.groupingBy(
                Item::getCode,
                Collectors.reducing(new Item(), (aggregatedItem, item) -> {
                    int aggregatedQuantity = aggregatedItem.getQuantity();
                    double aggregatedAmount = aggregatedItem.getAmount();
                    
                    aggregatedItem.setQuantity(aggregatedQuantity + item.getQuantity());
                    aggregatedItem.setAmount(aggregatedAmount + item.getAmount());
                    
                    return aggregatedItem;
                })
            ));
        
        System.out.println("Map total size: " + aggregatedItems.size()); // expected 4
        System.out.println();
        aggregatedItems.forEach((key, value) -> {
            System.out.println("key: " + key);
            System.out.println("value - quantity: " + value.getQuantity() + " - amount: " + value.getAmount());
            System.out.println();
        });
    }
    
    private static Item createItem(String code, int quantity, double amount) {
        Item item = new Item();
        item.setCode(code);
        item.setQuantity(quantity);
        item.setAmount(amount);
        return item;
    }
}

class Item {
    private String code;
    private int quantity;
    private double amount;
    
    public Item() {
        quantity = 0;
        amount = 0.0;
    }
    
    public String getCode() { return code; }
    public int getQuantity() { return quantity; }
    public double getAmount() { return amount; }
    
    public void setCode(String code) { this.code = code; }
    public void setQuantity(int quantity) { this.quantity = quantity; }
    public void setAmount(double amount) { this.amount = amount; }
}

以下是 output。

Map total size: 4

key: CODE2
value - quantity: 21 - amount: 157.0

key: CODE1
value - quantity: 21 - amount: 157.0

key: CODE4
value - quantity: 21 - amount: 157.0

key: CODE3
value - quantity: 21 - amount: 157.0

不得将输入 arguments 修改为Collectors.reducing new Item()只执行一次,所有归约操作将共享相同的“聚合实例”。 换句话说:map 将包含相同的值实例 4 次(您可以使用System.identityHashCode()或通过比较参考相等性轻松检查自己: aggregatedItems.get("CODE1") == aggregatedItems.get("CODE2") )。

相反,返回一个新的结果实例:

        final Map<String, Item> aggregatedItems = itemList
            .stream()
            .collect(Collectors.groupingBy(
                Item::getCode,
                Collectors.reducing(new Item(), (item1, item2) -> {
                    final Item reduced = new Item();
                    reduced.setQuantity(item1.getQuantity() + item2.getQuantity());
                    reduced.setAmount(item1.getAmount() + item2.getAmount());
                    return reduced;
                })
            ));

Output:

Map total size: 4

key: CODE2
value - quantity: 5 - amount: 64.0

key: CODE1
value - quantity: 1 - amount: 12.0

key: CODE4
value - quantity: 10 - amount: 31.0

key: CODE3
value - quantity: 5 - amount: 50.0

您正在使用reducing ,它假定您不会改变传入的累加器。 reducing不会为每个新组创建新Item ,并希望您创建新Item并将它们返回 lambda 中,就像这样:

// this works as expected
.collect(Collectors.groupingBy(
    Item::getCode,
    Collectors.reducing(new Item(), (item1, item2) -> createItem(
        item1.getCode(),
        item1.getQuantity() + item2.getQuantity(),
        item1.getAmount() + item2.getAmount()
    ))
));

因此,如果您使用数字或字符串等不可变对象,它非常适合。

由于您没有在代码中创建新的Item ,因此 reduce 会继续重用同一实例, reducing导致您看到的行为。

如果要改变对象,可以使用Collector.of以线程安全的方式进行可变归约:

.collect(Collectors.groupingBy(
    Item::getCode,
    Collector.of(Item::new, (aggregatedItem, item) -> {
        int aggregatedQuantity = aggregatedItem.getQuantity();
        double aggregatedAmount = aggregatedItem.getAmount();

        aggregatedItem.setQuantity(aggregatedQuantity + item.getQuantity());
        aggregatedItem.setAmount(aggregatedAmount + item.getAmount());
    }, (item1, item2) -> createItem(
        item1.getCode(),
        item1.getQuantity() + item2.getQuantity(),
        item1.getAmount() + item2.getAmount()
    ))
));

请注意,您现在将引用传递给Item的构造函数,即一种在必要时创建新Item的方法,而不仅仅是一个new Item() 此外,您还提供了第三个参数,combiner,它告诉收集器如何从两个现有项目中创建一个新项目,如果此收集器在并发情况下使用,则将使用该参数。 (有关组合器的更多信息,请参见此处

Collector.ofCollectors.reducing之间的这种对比与Stream.reduceStream.collect之间的对比相同。 在这里了解更多

可变归约与不可变归约

在这种情况下, Collectors.reducing()不是正确的工具,因为它意味着不可变归约,即执行折叠操作,其中每个归约步骤都会导致创建的不可变 object。

但是,不是在每个缩减步骤中生成新的 object,而是更改作为身份提供的 object 的 state 。

因此,您得到的结果不正确,因为每个线程只会创建一次身份object。 这个Item的单个实例用于累积,并在 map 的每个值中引用它。

您可以在Stream API 文档中找到更详细的信息,特别是在以下部分: ReductionMutable Reduction

这是一个简短的引述,解释了Stream.reduce()的工作原理( Collectors.reducing()背后的机制是相同的):

累加器 function获取部分结果和下一个元素,并产生新的部分结果

使用可变归约

这个问题可以通过生成一个新的Item实例来解决,同时累积映射到同一个,但更高效的方法是使用可变归约

为此,您可以实现通过 static 方法Collector.of()创建的自定义收集器:

Map<String, Item> aggregatedItems = itemList.stream()
    .collect(Collectors.groupingBy(
        Item::getCode,
        Collector.of(
            Item::new,   // mutable container of the collector
            Item::merge, // accumulator - defines how stream data should be accumulated
            Item::merge  // combiner - mergin the two containers while executing stream in parallel
        )
    ));

为方便起见,您可以引入方法merge()负责累积两个项目的属性。 它将允许避免在accumulatorcombiner中重复相同的逻辑,并保持收集器实现的精简和可读性。

public class Item {
    private String code;
    private int quantity;
    private double amount;
    
    // getters, constructor, etc.
    
    public Item merge(Item other) {
        this.quantity += other.quantity;
        this.amount += other.amount;
        return this;
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM