4
\$\begingroup\$

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<String> inputs = new HashSet<>();

    String getId() {
        return id;
    }

    void setId(String id) {

        this.id = id;
    }

    Set<String> 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<Foo> call(List<Foo> matchedFoos) {
        Map<String, List<Foo>> idToMatchedFooMap = matchedFoos.stream()
                .collect(groupingBy(Foo::getId, toList()));

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

        return mergedAndMatchedFoos;
    }

    private Foo mergeFoos(List<Foo> 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.

\$\endgroup\$
4
\$\begingroup\$

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<Foo> 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<Foo> 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<Foo> 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<String, Foo> instead a Map<String, List<Foo>>.

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

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.

\$\endgroup\$
  • \$\begingroup\$ A question about the javaDoc statement: "If the mapped keys contains duplicates (according to Object.equals(Object)), the value mapping function is applied to each equal element, and the results are merged using the provided merging function." To my understanding this means that the copy constructor and the merge function (foo1, foo2) -> { foo1.getInputs().addAll(foo2.getInputs()); return foo1; would only be applied to the equal foos. However, the use-case applies to foos with same ids, however, different inputs and therefore would be unequal. Thoughts? \$\endgroup\$ – arin Sep 5 '16 at 21:31
  • \$\begingroup\$ @arin The merge function is applied to values mapped to the same keys. Here the keys aren't the foos, but the ids of the foos (Foo::getId). So the merge function will be applied to two newly constructed foos, having the same id. \$\endgroup\$ – Tunaki Sep 5 '16 at 21:33
  • \$\begingroup\$ Got it, thanks for the clarification! @Tunaki \$\endgroup\$ – arin Sep 5 '16 at 21:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.