HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

Lambda to obtain a new merged object

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
obtainnewmergedobjectlambda

Problem

The requirement is to iterate over a list of Foos, and when 2 Foos have the same id merge their list of inputs together and obtain a new object.

I have achieved this with 2 streams and a private method to do this:

import java.util.HashSet;
import java.util.Set;

class Foo {
    private String id;
    private Set inputs = new HashSet<>();

    String getId() {
        return id;
    }

    void setId(String id) {

        this.id = id;
    }

    Set getInputs() {
        return inputs;

    }
}


and the GetMatchingFoos class:

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

import java.util.List;
import java.util.Map;

public class GetMatchingFoos {

    public List call(List matchedFoos) {
        Map> idToMatchedFooMap = matchedFoos.stream()
                .collect(groupingBy(Foo::getId, toList()));

        List mergedAndMatchedFoos = idToMatchedFooMap.values().stream()
                .map(this::mergeFoos)
                .collect(toList());

        return mergedAndMatchedFoos;
    }

    private Foo mergeFoos(List matchedFoos) {
        Foo result = new Foo();
        for(Foo matchedFoo : matchedFoos) {
            result.setId(matchedFoo.getId());
            result.getInputs().addAll(matchedFoo.getInputs());
        }
        return result;
    }

}


Can I obtain the same result by using only a single lambda operation that is more elegant than the above? I have looked into reducing but it was not very trivial.

Solution

To determine the result you're after, it is necessary to group by the id and hold a temporary map of the grouping operation.

You can make it a bit simpler, and not have a second Stream pipeline, by using the collectingAndThen built-in collector, which gives the ability to pass a finisher operation to the result of another collector.

Collection result = 
    matchedFoos.stream()
               .collect(groupingBy(Foo::getId, collectingAndThen(toList(), this::mergeFoos)))
               .values();


This code still collects all the foos having the same id into a list, but the use of collectingAndThen enables us to directly pass that list to mergeFoos and return the wanted Foo. Then, it is possible to retrieve the values by invoking values().

This will return a Collection as values() is defined to return that. If you really want a List, you can convert it easily to an ArrayList for example, with:

List mergedAndMatchedFoos = new ArrayList<>(result);


Note that the current code stores every foo having the same id in a list. This is a bit inefficient since, for the end result, we're interested in a single Foo where the inputs are all input of the foos in the list. So, instead, we can use the toMap(keyMapper, valueMapper, mergeFunction) collector and directly build the intermediate Map instead a Map>.

The key mapper returns the id of the foo; the value mapper returns a new Foo from a given one, and the merge function adds all inputs from one foo to the other.

Map result =
    matchedFoos.stream()
       .collect(Collectors.toMap(
          Foo::getId,
          Foo::new,
          (foo1, foo2) -> { foo1.getInputs().addAll(foo2.getInputs()); return foo1; }
       ));

List mergedAndMatchedFoos = new ArrayList<>(result.values());


This assumes that we have a copy-constructor Foo(Foo foo) constructing a Foo from another Foo (which is what the Foo::new method-reference refers to). The mutation of foo1 inside the merge function is safe to do since we're working on the new instance, constructed with the copy constructor.

Code Snippets

Collection<Foo> result = 
    matchedFoos.stream()
               .collect(groupingBy(Foo::getId, collectingAndThen(toList(), this::mergeFoos)))
               .values();
List<Foo> mergedAndMatchedFoos = new ArrayList<>(result);
Map<String, Foo> result =
    matchedFoos.stream()
       .collect(Collectors.toMap(
          Foo::getId,
          Foo::new,
          (foo1, foo2) -> { foo1.getInputs().addAll(foo2.getInputs()); return foo1; }
       ));

List<Foo> mergedAndMatchedFoos = new ArrayList<>(result.values());

Context

StackExchange Code Review Q#140431, answer score: 4

Revisions (0)

No revisions yet.