Home > Enterprise >  What's the most efficient way to combine objects in a list?
What's the most efficient way to combine objects in a list?

Time:08-12

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 foreach with if(containsKey(...)) and then convert it back to List. It just seems like an overkill.
Is there more elegant solution? I thought that flatMap from Stream API should do the thing, but I cannot figure out how.

UPD:
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 }]

CodePudding user response:

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

CodePudding user response:

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

CodePudding user response:

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

CodePudding user response:

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( 
                    ( Map.Entry < String, Integer > entry ) -> new StringInt( entry.getKey() , entry.getValue() ) 
                )
                .toList();
  • Related