Group, Sum byType then get diff using Java streams

2020-07-23 03:27发布

I would like to use stream to group, get the sum by type, then find the result of the different by type.

So this is my data set.

Sample(SampleId=1, SampleTypeId=1, SampleQuantity=5, SampleType=ADD), 
Sample(SampleId=2, SampleTypeId=1, SampleQuantity=15, SampleType=ADD), 
Sample(SampleId=3, SampleTypeId=1, SampleQuantity=25, SampleType=ADD), 
Sample(SampleId=4, SampleTypeId=1, SampleQuantity=5, SampleType=SUBTRACT), 
Sample(SampleId=5, SampleTypeId=1, SampleQuantity=25, SampleType=SUBTRACT) 
Sample(SampleId=6, SampleTypeId=2, SampleQuantity=10, SampleType=ADD), 
Sample(SampleId=7, SampleTypeId=2, SampleQuantity=20, SampleType=ADD), 
Sample(SampleId=8, SampleTypeId=2, SampleQuantity=30, SampleType=ADD), 
Sample(SampleId=9, SampleTypeId=2, SampleQuantity=15, SampleType=SUBTRACT), 
Sample(SampleId=10, SampleTypeId=2, SampleQuantity=35, SampleType=SUBTRACT)

My current method of doing is as such:

Map<Long, Integer> result = sampleList.stream()
.collect(
    Collectors.groupingBy(
        Sample::getSampleTypeId, 
        Collectors.summingInt(Sample::getSampleQuantity)
        )
    );

and my result is as such (SampleTypeId, result):

{1=75, 2=110}

I'm not sure how to continue from here. Basically, there is a SampleType of either ADD or SUBTRACT. So I need to SUM all the TypeId=1 and Type=ADD AND SUM all the typeId=1 and Type=SUBSTRACT then find the difference.

Example

1 >> (5+15+25) - (5+25) = 15
2 >> (10+20+30) - (15+35) = 10

So the expected output should be {1=15, 2=10}.

4条回答
ゆ 、 Hurt°
2楼-- · 2020-07-23 03:53

Collectors are a very powerful feature in Java 8, because each of them can be composed of different blocks, and even then, there is a simple way to compose collectors themselves.

This solution is solved by such Collector composition, below I'll guide you through the steps towards the solution.

First of all, we start from the first requirement: the result has to be a dictionary keyed with TypeId:

Map<TypeId, List<Sample>> r = stream.collect(
  Collectors.groupingBy(Sample::getTypeId)
);

Second, among each TypeId list we need to make sub-groups by Type. For that, we use an extended version of Grouping By collector, which allows us to specify what we should do once we classified elements and assigned them to groups. In out case, we need to classify them again:

Map<TypeId, Map<Type, List<Sample>>> r = stream.collect(
  groupingBy(Sample::getTypeId,
    groupingBy(Sample::getType))
);

Now, once both classifiers are done, we need to sum quantities among all the elements within group. The technique is the same: we tell what to do with group elements after classifier is run. In this case, sum all the amounts:

Map<TypeId, Map<Type, Integer>> r = stream.collect(
  groupingBy(Sample::getTypeId,
    groupingBy(Sample::getType, Collectors.summingInt(Sample::getQuantity))
  )
);

Next, the most interesting part: we need to do an operation on the Map<Type, Integer>. More specifically, we need to do map[Add] - map[Sub]. The most important notion of this step is we want to do it once all the other collecting operations are complete, before returning result. This is an operation which can only happen at the stage of the stream collection called Collector.finisher. We can compose finishers of arbitrary collectors by calling collectingAndThen method of Collectors class:

Map<TypeId, Integer> r = stream.collect(
  groupingBy(Sample::getTypeId,
    Collectors.collectingAndThen(
      groupingBy(Sample::getType, summingInt(Sample::getQuantity)),
      map -> map.getOrDefault(SampleType.ADD, 0) - map.getOrDefault(SampleType.SUBTRACT, 0)
    )
  )
);

That's all. Note that it's important to call getOrDefault instead of get, because there might not've been any elements with some specific SampleType, and map would otherwise return null, causing the whole thing to collapse with NullPointerException.

查看更多
来,给爷笑一个
3楼-- · 2020-07-23 04:00
sampleList.stream()
        .collect(Collectors.groupingBy(Sample::getSampleTypeId,
                Collectors.collectingAndThen(Collectors.groupingBy(Sample::getSampleType,
                        Collectors.summingInt(Sample::getSampleQuantity)),
                        map -> (map.getOrDefault(SampleType.ADD, 0) 
                                - map.getOrDefault(SampleType.SUBTRACT, 0)))));

The outer groupingBy groups by sampleTypeId. At the next level, it involves the construction of map with key SampleType and value the sum of SampleQuantity for a key. The finisher of the collectingAndThen subtracts the value of ADD and SUBTRACT to give the final result.

Version 2 (even simpler):

sampleList.stream()
        .collect(Collectors.groupingBy(Sample::getSampleTypeId,
                Collectors.summingInt(sample -> sample.getSampleType() == SampleType.SUBTRACT
                        ? -sample.getSampleQuantity() :
                        sample.getSampleQuantity()
                )));

It does not construct the second map that was constructed earlier. It simply checks whether the SampleType is ADD or SUBTRACT and returns the sampleQuantity appropriately.

查看更多
甜甜的少女心
4楼-- · 2020-07-23 04:19

There may be a simpler way with just map:

Map<Long, Integer> result = sampleList.stream()
.map(sample->{if(sample.getSampleType==SUBTRACT)
     return new Sample((sample.SampleId, 
                        sample.SampleTypeId, 
                        sample.SampleQuantity*(-1), 
                        sample.SampleType))

     return sample;
 })
.collect(
    Collectors.groupingBy(
        Sample::getSampleTypeId, 
        Collectors.summingInt(Sample::getSampleQuantity)
        )
    );
查看更多
冷血范
5楼-- · 2020-07-23 04:20

The following:

1 >> ( 5 + 15 + 25) - ( 5 + 25) = 15
2 >> (10 + 20 + 30) - (15 + 35) = 10

is exactly the same as this:

1 >>  5 + 15 + 25 -  5 - 25 = 15
2 >> 10 + 20 + 30 - 15 - 35 = 10

So you could take advantage of this and avoid nested-grouping by SampleType. Instead, just either sum or subtract on the downstream collector:

Map<Long, Integer> result = sampleList.stream()
    .collect(Collectors.groupingBy(
        Sample::getSampleTypeId, 
        Collectors.summingInt(sample -> sample.getSampleQuantity() * 
                (SampleType.ADD.equals(sample.getSampleType()) ? 1 : -1))));

Even better, if you could create a method in the Sample class:

public int getCalculatedQuantity() {
    return sampleQuantity * (SampleType.ADD.equals(sampleType) ? 1 : -1);
}

Then use it as follows:

Map<Long, Integer> result = sampleList.stream()
    .collect(Collectors.groupingBy(
        Sample::getSampleTypeId, 
        Collectors.summingInt(Sample::getCalculatedQuantity)));
查看更多
登录 后发表回答