Group, Collectors, Map (Int to String), Map (Map t

2020-07-29 03:54发布

问题:

This is a continuation of my previous question at Group, Sum byType then get diff using Java streams.

As suggested, I should post as a separate thread instead of updating the original one.

So with my previous set of question, I have achieved that, and now, with the continuation.

Background:

I have the following dataset

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)

I am currently using this:

sampleList.stream()
    .collect(Collectors.groupingBy(Sample::getTypeId,
        Collectors.summingInt(
            sample -> SampleType.ADD.equalsIgnoreCase(sample.getSampleType())
                ? sample.getSampleQuantity() :
                -sample.getSampleQuantity()
            )));

And also this

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))));

as the accepted answer to get the desired output to group in a Map<Long, Integer>:

{1=15, 2=10}

With that, I was wondering, if this could be expanded into something more.

First, how could I have it return as a Map<String, Integer> instead of the original Map<Long, Integer>. Basically, for the SampleTypeId; 1 refers to HELLO, 2 refers to WORLD.

So I would need like a .map (or maybe other function) to transform the data from 1 to HELLO and 2 to WORLD by calling a function say convertType(sampleTypeId)?. So the expected output would then be {"HELLO"=15, "WORLD"=10}. Is that right? How should I edit the current suggested solution to this?

Lastly, I would like to know if it is also possible to return it to a Object instead of a Map. So let's say I have a Object; SummaryResult with (String) name and (int) result. So it returns a List<SummaryResult> instead of the original Map<Long, Integer>. How can I use the .map (or other) feature to do this? Or is there other way to doing so? The expected output would be something along this line.

SummaryResult(name="hello", result=15), 
SummaryResult(name="world", result=10), 

Would really appreciate it with the explanation in steps as given previously by @M. Prokhorov.

Update:

After updating to

sampleList.stream()
        .collect(Collectors.groupingBy(sample -> convertType(sample.getSampleTypeId()),
                Collectors.collectingAndThen(
                    Collectors.groupingBy(Sample::getSampleType,
                        Collectors.summingInt(Sample::getSampleQuantity)),
                            map -> map.getOrDefault(SampleType.ADD, 0)
                                    - map.getOrDefault(SampleType.SUBTRACT, 0))));

private String convertType(int id) {
    return (id == 1) ? "HELLO" : "WORLD";
}

回答1:

For first part, considering you have somewhere the method

String convertType(int typeId)

You simply need to change first classifier from this

groupingBy(SampleType::getTypeId)

to this

groupingBy(sample -> convertType(sample.getTypeId()))

Everything else remains the same.

Latter type is a little trickier, and technically doesn't benefit from it being a stream-related solution at all.

What you need is this:

public List<SummaryResult> toSummaryResultList(Map<String, Integer> resultMap) {
  List<SummaryResult> list = new ArrayList<>(resultMap.size());
  for (Map.Entry<String, Integer> entry : resultMap.entrySet()) {
    String name = entry.getKey();
    Integer value = entry.getValue();

    // replace below with construction method you actually have
    list.add(SummaryResult.withName(name).andResult(value));
  }
  return list;
}

You can use this as part of collector composition, where your whole collector will get wrapped into a collectingAndThen call:

collectingAndThen(
  groupingBy(sample -> convertType(sample.getTypeId()),
     collectingAndThen(
       groupingBy(Sample::getSampleType,
         summingInt(Sample::getSampleQuantity)),
         map -> map.getOrDefault(SampleType.ADD, 0)
                - map.getOrDefault(SampleType.SUBTRACT, 0))),
  result -> toSummaryResultList(result))

However, as you can see, it is the whole collector that gets wrapped, so there is no real benefit in my eyes to the above version to a simpler and easier to follow (at least to me) version below that uses an intermediate variable, but isn't so much of a wall of code:

// do the whole collecting thing like before
Map<String, Integer> map = sampleList.stream()
    .collect(Collectors.groupingBy(sample -> convertType(sample.getTypeId()),
            Collectors.collectingAndThen(
                Collectors.groupingBy(Sample::getSampleType,
                    Collectors.summingInt(Sample::getSampleQuantity)),
                        map -> map.getOrDefault(SampleType.ADD, 0)
                                - map.getOrDefault(SampleType.SUBTRACT, 0))));

// return the "beautified" result
return toSummaryResultList(map);

Another point to consider in above is: convertType method will be called as many times as there are elements in sampleList, so if convertType call is "heavy" (for example, uses database or IO), then it's better to call it as part of toSummaryResultList conversion, not as stream element classifier. In which case you will be collecting from map of type Map<Integer, Integer> still, and using convertType inside the loop. I will not add any code with this in consideration, as I view this change as trivial.



回答2:

You could indeed use a map() function

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))))
        .entrySet()            
        .stream()            
        .map(entry->new SummaryResult(entry.getKey()),entry.getValue())
        .collect(Collectors.toList());


回答3:

ToIntFunction<Sample> signedQuantityMapper= sample -> sample.getQuantity() 
    * (sample.getType() == Type.ADD ? 1 : -1);

Function<Sample, String> keyMapper = s -> Integer.toString(s.getTypeId());

Map<String, Integer> result = sampleList.stream().collect(
    Collectors.groupingBy(
        keyMapper,
        Collectors.summingInt(signedQuantityMapper)));