简体   繁体   中英

Java 8 Collector.groupingBy classifier value in downstream

What will be a proper way to provide classifier value into supplier function of the collector in the following example:

import static java.math.BigDecimal.*;
import static java.util.stream.Collector.*;
import static java.util.stream.Collectors.*;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Test {

    public static class Item {
        String key;
        BigDecimal a;
        BigDecimal b;
        public Item(String key, BigDecimal a, BigDecimal b) {
            this.key = key;
            this.a = a;
            this.b = b;
        }
        public String getKey() {
            return key;
        }
        public BigDecimal getA() {
            return a;
        }
        public BigDecimal getB() {
            return b;
        }


    }
    public static class ItemSum {
        public ItemSum() {
        }

        public ItemSum(String key) {
            this.key = key;
        }
        String key;
        BigDecimal sumA = ZERO;
        BigDecimal sumB = ZERO;
        public void add(BigDecimal a, BigDecimal b) {
            sumA = sumA.add(a);
            sumB = sumB.add(b);
        }

        public ItemSum merge(ItemSum is) {
            sumA = sumA.add(is.getSumA());
            sumB = sumB.add(is.getSumB());
            return this;
        }
        public BigDecimal getSumA() {
            return sumA;
        }
        public BigDecimal getSumB() {
            return sumB;
        }


    }

    public static void main(String[] args) {

        Map<String, ItemSum> map = list().stream().collect(
                groupingBy(Item::getKey, 
                        of(
                                ItemSum::new, 
                                (s,i) -> {s.add(i.getA(), i.getB());},
                                (i,j) -> {return i.merge(j);}
                                )
                        )
                );
        map.forEach((k,v) -> {System.out.println(String.format("%s: A: %s: B: %s", k, v.getSumA(), v.getSumB()));});
    }

    public static List<Item> list() {
        List<Item> list = new ArrayList<>(3);
        list.add(new Item("a", ONE, ONE));
        list.add(new Item("a", ONE, ONE));
        list.add(new Item("b", ONE, ONE));
        return list;
    }

}

The problem is to pass key value into ItemSum during construction time. I've got some workarounds to populate key field afterwards but I'm wondering if there is a way to do it in supplier and eliminate default constructor of ItemSum .

This is a job for the toMap(keyMapper, valueMapper, mergeFunction) collector, not the groupingBy collector.

Let's add a constructor

public ItemSum(Item item) {
    this.key = item.getKey();
    this.sumA = item.getA();
    this.sumB = item.getB();
}

to the class ItemSum . This constructor initializes one ItemSum based on an Item . You can then remove the default constructor (it won't be needed). Then you can simply have the following:

Map<String, ItemSum> map = 
    list().stream()
          .collect(Collectors.toMap(Item::getKey, ItemSum::new, ItemSum::merge));

What this does is that is collects each Item element into a map classified by the key of each Item . For the value, we initialize it with a single ItemSum . When multiple values are encountered for the same key, we merge them together.

In short, you can't do it the way you want.

If you make your ItemSum class receive an Item instance in your add method (the accumulator method of the collector), you could easily achieve what you want:

public class ItemSum {

    String key;

    BigDecimal sumA = ZERO;

    BigDecimal sumB = ZERO;

    public void add(Item item) {
        key = item.getKey();
        sumA = sumA.add(item.getA());
        sumB = sumB.add(item.getB());
    }

    public ItemSum merge(ItemSum is) {
        sumA = sumA.add(is.getSumA());
        sumB = sumB.add(is.getSumB());
        return this;
    }

    public BigDecimal getSumA() {
        return sumA;
    }

    public BigDecimal getSumB() {
        return sumB;
    }
}

Then, use it this way:

Map<String, ItemSum> map = list().stream()
    .collect(Collectors.groupingBy(
        Item::getKey, 
        Collector.of(
            ItemSum::new, 
            ItemSum::add,
            ItemSum::merge)));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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