4

Let's say I have the following list.

List<StringInteger> test = new ArrayList<>(); //StringInteger is just a pojo with String and int
test.add(new StringInteger("a", 1));
test.add(new StringInteger("b", 1));
test.add(new StringInteger("a", 3));
test.add(new StringInteger("c", 1));
test.add(new StringInteger("a", 1));
test.add(new StringInteger("c", -1));

System.out.println(test); // [{ a : 1 }, { b : 1 }, { a : 3 }, { c : 1 }, { a : 1 }, { c : -1 }]

I need to write a method that would unite items by String key and add integers. So that the result list would be [{ a : 5 }, { b : 1 }, { c : 0 }]

I could do it using HashMap, but if I go that way - I'll have to create a Map, then use enhanced for-loop with if(containsKey(...)) and then convert it back to List. It just seems like an overkill.

Is there a more elegant solution? I thought that flatMap from Stream API should do the thing, but I cannot figure out how.

Here's my clumsy solution. It works, but I believe that it can be done more simple than that.

Map<String, Integer> map = new HashMap<>();
for (StringInteger stringInteger : test) {
    if (map.containsKey(stringInteger.getKey())) {
        int previousValue = map.get(stringInteger.getKey());
        map.put(stringInteger.getKey(), previousValue + stringInteger.getValue());
    } else {
        map.put(stringInteger.getKey(), stringInteger.getValue());
    }
}

List<StringInteger> result = map.entrySet()
    .stream()
    .map(stringIntegerEntry -> new StringInteger(stringIntegerEntry.getKey(), stringIntegerEntry.getValue()))
    .collect(Collectors.toList());

System.out.println(result); // [{ a : 5 }, { b : 1 }, { c : 0 }]
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46

4 Answers4

4

The simplest way to accomplish this is likely

List<StringInteger> combined = test.stream()
  .collect(
     Collectors.groupingBy(
        StringInteger::getKey,
        Collectors.summingInt(StringInteger::getValue)))
  .entrySet()
  .stream()
  .map(entry -> new StringInteger(entry.getKey(), entry.getValue()))
  .toList();
Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
4

Here is a full example with code based on the code seen in two good Answers by Ivanchenko and by Wasserman.

Here, we use a record in Java 16+ to define your StringInt class.

The name StringInt is used rather than StringInteger, to stress that we have a primitive int as the member field type rather than Integer class as the type.

I should think a Map < String , Integer > would suffice for your goal.

record StringInt( String string , int integer ) { }

List < StringInt > inputs =
        List.of(
                new StringInt( "a" , 1 ) ,
                new StringInt( "b" , 1 ) ,
                new StringInt( "a" , 3 ) ,
                new StringInt( "c" , 1 ) ,
                new StringInt( "a" , 1 ) ,
                new StringInt( "c" , - 1 )
        );

Map < String, Integer > results =
        inputs
                .stream()
                .collect(
                        Collectors.groupingBy(
                                StringInt :: string ,                          // Key
                                Collectors.summingInt( StringInt :: integer )  // Value
                        ) );

results = {a=5, b=1, c=0}

Or, if you insist on instantiating StringInt objects as the result:

record StringInt( String string , int integer ) { }

List < StringInt > inputs =
        List.of(
                new StringInt( "a" , 1 ) ,
                new StringInt( "b" , 1 ) ,
                new StringInt( "a" , 3 ) ,
                new StringInt( "c" , 1 ) ,
                new StringInt( "a" , 1 ) ,
                new StringInt( "c" , - 1 )
        );

List < StringInt > results =
        inputs
                .stream()
                .collect(
                        Collectors.groupingBy(
                                StringInt :: string ,                          // Key
                                Collectors.summingInt( StringInt :: integer )  // Value
                        ) )
                .entrySet()                                                    // Returns a Set < Entry < String , Integer > >
                .stream()
                .map( 
                    entry -> new StringInt( entry.getKey() , entry.getValue() ) 
                )
                .toList();
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • When there's no issue with type inference, good practice is to avoid explicit types with lambda arguments. If you feel like it might be not obvious what would be the type of the stream element, it's better to simplify the stream extracting the part that generates a map into a separate method rather than providing explicit types which doesn't reduce complexity (in case if you not agree with this editing, please rollback). – Alexander Ivanchenko Aug 12 '22 at 04:12
  • @AlexanderIvanchenko I had indeed gone out of my way to insert the explicit type of the lambda argument because the type is not immediately obvious, especially for a student learning streams and lambdas. But in this case I’ll yield to your judgement. And I can see how the context with its `getKey` and `getValue` methods might suffice. – Basil Bourque Aug 12 '22 at 07:33
3

As @LouisWasserman said in the comment, HashMap is the right tool for this task.

To translate the whole code into a stream, you can use the built-in collector groupingBy() in conjunction with summingInt as the downstream collector grouping.

result = test.stream()
    .collect(Collectors.groupingBy(   // creates an intermediate map Map<String, Integer>
        StringInteger::getKey,                         // mapping a key
        Collectors.summingInt(StringInteger::getValue) // generating a value
    ))
    .entrySet().stream()
    .map(entry -> new StringInteger(entry.getKey(), entry.getValue()))
    .toList();
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
2

Here's my clumsy solution. It works, but I believe that it can be done more simple than that.

Just to show you, you can do the adding up in the map more neatly, without using streams:

for (StringInteger stringInteger : test) {
    map.merge(stringInteger.getKey(), stringInteger.getValue(), Integer::sum);
}
Andy Turner
  • 137,514
  • 11
  • 162
  • 243