ddd
ddd

Reputation: 5029

How to obtain new list after groupby and sum over one attribute

I have a list of Settlement class which has the following attributes:

public class Settlement {
    private String contractNo;
    private String smartNo;
    private String dealTrackNo;
    private String buySellFlag;
    private String cashFlowType;
    private String location;
    private String leaseNo;
    private String leaseName;
    private double volume;
    private double price;
    private double settleAmount;

    // getters and setters
}

Now I would like to group the list of Settlement by SmartNo (String) and get the sum over settleAmount which becomes the new settleAmount for each SmartNo.

Since I am using Java 8, stream should be the way to go.

Groupby should be quite straight forward using the following code:

Map<String, List<Settlement>> map = list.stream()
              .collect(Collectors.groupingBy(Settlement::getSmartNo));
System.out.println(map.getValues());

What if I want to get a new list after grouping by SmartNo and summing over settlementAmount? Most of the examples out there only shows how to print out the sums. What I am interested is how to get the aggregated list?

Upvotes: 0

Views: 1686

Answers (2)

Anonymous
Anonymous

Reputation: 86343

I think the not-too-complex way through is a new stream on each member of the values() of your map and then a map() and reduce(). I am mapping to a new class AggregatedSettlement with just the three fields smartNo, volume and settleAmount (the last will be the sum). And then reducing by summing the settleAmounts.

    List<AggregatedSettlement> aggregatedList = list.stream()
            .collect(Collectors.groupingBy(Settlement::getSmartNo))
            .values()
            .stream()
            .map(innerList -> innerList.stream()
                    .map(settlm -> new AggregatedSettlement(settlm.getSmartNo(), 
                            settlm.getVolume(), settlm.getSettleAmount()))
                    .reduce((as1, as2) -> {
                        if (as1.getVolume() != as2.getVolume()) {
                            throw new IllegalStateException("Different volumes " + as1.getVolume() 
                                    + " and " + as2.getVolume() + " for smartNo " + as1.getSmartNo());
                        }
                        return new AggregatedSettlement(as1.getSmartNo(), as1.getVolume(), 
                                as1.getSettleAmount() + as2.getSettleAmount());
                    })
                    .get()
            )
            .collect(Collectors.toList());

I am not too happy about the call to get() on the Optional<AggregatedSettlement> that I get from reduce(); usually you should avoid get(). In this case I know that the original grouping only produced lists of at least one element, so the the reduce() cannot give an empty optional, hence the call to get() will work. A possible refinement would be orElseThrow() and a more explanatory exception.

I am sure there’s room for optimization. I am really producing quite many more AggregatedSettlement objects than we need in the end. As always, don’t optimize until you know you need to.

Edit: If only for the exercise here’s the version that doesn’t construct superfluous AggregatedSettlement objects. Instead it creates two streams on each list from your map, and it’s 5 lines longer:

    List<AggregatedSettlement> aggregatedList = list.stream()
            .collect(Collectors.groupingBy(Settlement::getSmartNo))
            .entrySet()
            .stream()
            .map(entry -> {
                double volume = entry.getValue()
                        .stream()
                        .mapToDouble(Settlement::getVolume)
                        .reduce((vol1, vol2) -> {
                            if (vol1 != vol2) {
                                throw new IllegalStateException("Different volumes " + vol1 
                                        + " and " + vol2 + " for smartNo " + entry.getKey());
                            }
                            return vol1;
                        })
                        .getAsDouble();
                double settleAmountSum = entry.getValue()
                        .stream()
                        .mapToDouble(Settlement::getSettleAmount)
                        .sum();
                return new AggregatedSettlement(entry.getKey(), volume, settleAmountSum);
            })
            .collect(Collectors.toList());

Pick the one you find easier to read.

Edit 2: It seems from this answer that in Java 9 I will be able to avoid the call to Optional.get() if instead of map() I use flatMap() and instead of get() I use stream(). It will be 6 chars longer, I may still prefer it. I haven’t tried Java 9 yet, though (now I know what I’m going to do today :-) The advantage of get() is of course that it would catch a programming error where the inner list comes out empty.

Upvotes: 2

Tagir Valeev
Tagir Valeev

Reputation: 100279

If I understand the question correctly, you need a toMap collector with custom merger like this:

list.stream().collect(Collectors.toMap(
       Settlement::getSmartNo,
       Function.identity(),
       (s1, s2) -> s1.addAmount(s2.getSettleAmount())));

With a helper method inside Settlement class:

Settlement addAmount(double addend) {
    this.settleAmount += addend;
    return this;
}

Upvotes: 2

Related Questions