web-dev-qa-db-ja.com

Java 8つのストリームを3つのフィールドでグループ化し、合計とカウントで集計すると、1行の出力が生成されます

フォーラムで同様の質問があったことは知っていますが、どれも私の問題に完全に対処していないようです。今、私はJava 8に慣れていないので、ご容赦ください。次のような製品のリストがあります。

Input:
name    category    type    cost
prod1       cat2     t1      100.23
prod2       cat1     t2      50.23
prod1       cat1     t3      200.23
prod3       cat2     t1      150.23
prod1       cat2     t1      100.23


Output:
Single line (name, category, type) summing the cost and count of products.




Product {
    public String name;
    public String category;
    public String type;
    public int id;
    public double cost;

}

これを名前、カテゴリ、タイプでグループ化し、このデータを要約して各製品の総コストと数を生成する単一の結果を生成する必要があります。ほとんどの例は、2つのフィールドによるグループ化と、単一の基準を使用した集計を示しています。

フォーラムでの提案に続いて、私はグループ化のためにこれを思いついた:

    public class ObjectKeys {

    ArrayList<Object> keys;

    public ObjectKeys(Object...searchKeys) {

         keys = new ArrayList<Object>();

            for (int i = 0; i < searchKeys.length; i++) {
                keys.add( searchKeys[i] );
            }
    }

}

次に、次のように使用しました。

Map<String, Map<String, Map<String, List<Product>>>> productsByNameCategoryType =
    products.stream().collect(groupingBy(new ObjectKeys(l.name(), l.category(),l.type())))

しかし、カウントと合計を上記のコードにチェーンするにはどうすればよいですか?特に2つ以上のフィールドを持つgroupbyの場合。これを行うためのより良い方法はありますか?

私が言ったように、私のJava8はそれほど良くありません、助けてください。

3
Vuzi

前提条件

class Product {
    public String name;
    public String category;
    public String type;
    public int id; 
    //todo:implement equals(), toString() and hashCode()
 }

class Item{
   public Product product;
   public double cost;
}

要約方法

Collectors#groupingByCollectors#summarizingDouble を使用して、商品ごとにグループ化されたアイテムを要約できます。

List<Item> items = ...; 
Map<Product, DoubleSummaryStatistics> stat = items.stream().collect(groupingBy(
            it -> it.product,
            Collectors.summarizingDouble(it -> it.cost)
));

// get some product summarizing
long count = stat.get(product).getCount();
double sum = stat.get(product).getSum();

//list all product summarizing
stat.entrySet().forEach(it ->
  System.out.println(String.format("%s - count: %d, total cost: %.2f"
        , it.getKey(),it.getValue().getCount(), it.getValue().getSum()));
);

同じ商品のアイテムをマージします

まず、qtyクラスにItemフィールドを追加する必要があります。

class Item{
   public int qty;
   //other fields will be omitted

   public Item add(Item that) {
        if (!Objects.equals(this.product, that.product)) {
            throw new IllegalArgumentException("Can't be added items"
                     +" with diff products!");
        }
        return from(product, this.cost + that.cost, this.qty + that.qty);
    }

    private static Item from(Product product, double cost, int qty) {
        Item it = new Item();
        it.product = product;
        it.cost = cost;
        it.qty = qty;
        return it;
    }

}

次に、 Collectors#toMap を使用して、アイテムを同じ商品とマージできます。

Collection<Item> summarized = items.stream().collect(Collectors.toMap(
        it -> it.product,
        Function.identity(),
        Item::add
)).values();

最後に

両方の方法で同じことを行うことがわかりますが、2番目のアプローチはストリームでの操作が簡単です。 githubでチェックインした2つの方法のテストでは、クリックして詳細を確認できます。 アイテムの要約アイテムのマージ 方法。

5
holi-java

迅速で汚い解決策は次のとおりです。

_    Map<String, String> productsByNameCategoryType = products.stream()
            .collect(Collectors.groupingBy(p 
                            -> p.getName() + '-' + p.getCategory() + '-' + p.getType(),
                    Collectors.collectingAndThen(
                            Collectors.summarizingDouble(Product::getCost),
                            dss -> String.format("%7.2f%3d", 
                                                 dss.getSum(), dss.getCount()))));
_

結果マップのキーと値の両方に対して独自のクラスを作成することをお勧めします。いずれにせよ、データと上記のコードを使用すると、マップには4つのエントリが含まれます。

_prod1-cat1-t3:  200,23  1
prod1-cat2-t1:  200,46  2
prod3-cat2-t1:  150,23  1
prod2-cat1-t2:   50,23  1
_

私のコンピューターにはデンマーク語のロケールがあるため、合計は小数点としてコンマで出力されます(必要に応じて、ロケールをString.format()に渡してロケールを制御できます)。

あなたの友達はCollectors.collectingAndThen()Collectors.summarizingDouble()の組み合わせです。私はそれを この答え から取った。

3
Ole V.V.