Lambda to obtain a new merged object
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&lt;Foo&gt; 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&lt;Foo&gt; 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&lt;Foo&gt; mergedAndMatchedFoos = new ArrayList&lt;&gt;(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&lt;String, Foo&gt; instead a Map&lt;String, List&lt;Foo&gt;&gt;.

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&lt;String, Foo&gt; result =
    matchedFoos.stream()
       .collect(Collectors.toMap(
          Foo::getId,
          Foo::new,
          (foo1, foo2) -&gt; { foo1.getInputs().addAll(foo2.getInputs()); return foo1; }
       ));

List&lt;Foo&gt; mergedAndMatchedFoos = new ArrayList&lt;&gt;(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.