The groupingBy operation (or something similar) is unavoidable, the Map created by the operation is also used during the operation for looking up the grouping keys and finding the duplicates. But you can combine it with the reduction of the group elements:

Map<String, Friend> uniqueFriendMap = friends.stream()
    .collect(Collectors.groupingBy(Friend::uniqueFunction,
        Collectors.collectingAndThen(
            Collectors.reducing((a,b) -> friendMergeFunction(a,b)), Optional::get)));

The values of the map are already the resulting distinct friends. If you really need a List, you can create it with a plain Collection operation:

List<Friend> mergedFriends = new ArrayList<>(uniqueFriendMap.values());

If this second operation still annoys you, you can hide it within the collect operation:

List<Friend> mergedFriends = friends.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Friend::uniqueFunction, Collectors.collectingAndThen(
            Collectors.reducing((a,b) -> friendMergeFunction(a,b)), Optional::get)),
        m -> new ArrayList<>(m.values())));

Since the nested collector represents a Reduction (see also this answer), we can use toMap instead:

List<Friend> mergedFriends = friends.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toMap(Friend::uniqueFunction, Function.identity(),
            (a,b) -> friendMergeFunction(a,b)),
        m -> new ArrayList<>(m.values())));

Depending on whether friendMergeFunction is a static method or instance method, you may replace (a,b) -> friendMergeFunction(a,b) with DeclaringClass::friendMergeFunction or this::friendMergeFunction.


But note that even within your original approach, several simplifications are possible. When you only process the values of a Map, you don’t need to use the entrySet(), which requires you to call getValue() on each entry. You can process the values() in the first place. Then, you don’t need the verbose input -> { return expression; } syntax, as input -> expression is sufficient. Since the groups of the preceding grouping operation can not be empty, the filter step is obsolete. So your original approach would look like:

Map<String, List<Friend>> uniqueFriendMap
    = friends.stream().collect(Collectors.groupingBy(Friend::uniqueFunction));
List<Friend> mergedFriends = uniqueFriendMap.values().stream()
    .map(group -> group.stream().reduce((a,b) -> friendMergeFunction(a,b)).get())
    .collect(Collectors.toList());

which is not so bad. As said, the fused operation doesn’t skip the Map creation as that’s unavoidable. It only skips the creations of the Lists representing each group, as it will reduce them to a single Friend in-place.

Answer from Holger on Stack Overflow
Top answer
1 of 1
19

The groupingBy operation (or something similar) is unavoidable, the Map created by the operation is also used during the operation for looking up the grouping keys and finding the duplicates. But you can combine it with the reduction of the group elements:

Map<String, Friend> uniqueFriendMap = friends.stream()
    .collect(Collectors.groupingBy(Friend::uniqueFunction,
        Collectors.collectingAndThen(
            Collectors.reducing((a,b) -> friendMergeFunction(a,b)), Optional::get)));

The values of the map are already the resulting distinct friends. If you really need a List, you can create it with a plain Collection operation:

List<Friend> mergedFriends = new ArrayList<>(uniqueFriendMap.values());

If this second operation still annoys you, you can hide it within the collect operation:

List<Friend> mergedFriends = friends.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Friend::uniqueFunction, Collectors.collectingAndThen(
            Collectors.reducing((a,b) -> friendMergeFunction(a,b)), Optional::get)),
        m -> new ArrayList<>(m.values())));

Since the nested collector represents a Reduction (see also this answer), we can use toMap instead:

List<Friend> mergedFriends = friends.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toMap(Friend::uniqueFunction, Function.identity(),
            (a,b) -> friendMergeFunction(a,b)),
        m -> new ArrayList<>(m.values())));

Depending on whether friendMergeFunction is a static method or instance method, you may replace (a,b) -> friendMergeFunction(a,b) with DeclaringClass::friendMergeFunction or this::friendMergeFunction.


But note that even within your original approach, several simplifications are possible. When you only process the values of a Map, you don’t need to use the entrySet(), which requires you to call getValue() on each entry. You can process the values() in the first place. Then, you don’t need the verbose input -> { return expression; } syntax, as input -> expression is sufficient. Since the groups of the preceding grouping operation can not be empty, the filter step is obsolete. So your original approach would look like:

Map<String, List<Friend>> uniqueFriendMap
    = friends.stream().collect(Collectors.groupingBy(Friend::uniqueFunction));
List<Friend> mergedFriends = uniqueFriendMap.values().stream()
    .map(group -> group.stream().reduce((a,b) -> friendMergeFunction(a,b)).get())
    .collect(Collectors.toList());

which is not so bad. As said, the fused operation doesn’t skip the Map creation as that’s unavoidable. It only skips the creations of the Lists representing each group, as it will reduce them to a single Friend in-place.

🌐
Baeldung
baeldung.com › home › java › java streams › merging streams in java
Merging Streams in Java | Baeldung
June 24, 2026 - @Test public void whenMergingStreams_thenResultStreamContainsElementsFromBoth() { Stream<Integer> stream1 = Stream.of(1, 3, 5); Stream<Integer> stream2 = Stream.of(2, 4, 6); Stream<Integer> resultingStream = Stream.concat(stream1, stream2); assertEquals( Arrays.asList(1, 3, 5, 2, 4, 6), resultingStream.collect(Collectors.toList())); } When we need to merge more than 2 Streams, things become a bit more complex.
Discussions

Java 8 stream - merge collections of objects sharing the same Id - Stack Overflow
I have a collection of invoices : class Invoice { int month; BigDecimal amount } I'd like to merge these invoices, so I get one invoice per month, and the amount is the sum of the invoices amo... More on stackoverflow.com
🌐 stackoverflow.com
Combine some fields of two objects by common id using stream
Some additional questions: Are there always the same IDs in both lists? If not, what is the expected behavior for those IDs, where only one kind of object is present? Is there only one instance of a given kind of object, having the same ID, or might a given ID be present multiple times? I guess I'd transform both lists to maps, id as key. If there are IDs, that are only in one or the other, I'd create a set of the IDs, where I put both key-sets of the maps into, then iterating the set of IDs and combining the results. If the IDs are the same and always present in both, you could also just transform one list into a map and then while iterating the other list matching the values. More on reddit.com
🌐 r/javahelp
2
1
January 13, 2020
java - Lambda to obtain a new merged object - Code Review Stack Exchange
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 m... More on codereview.stackexchange.com
🌐 codereview.stackexchange.com
December 9, 2016
Merging lists under same objects in a list using Java streams - Stack Overflow
I have two objects like following: public class A { private Integer id; private String name; private List list; public A(Integer id, String name, List list) { ... More on stackoverflow.com
🌐 stackoverflow.com
March 6, 2018
🌐
Coderanch
coderanch.com › t › 691517 › java › combine-objects-Streams
How can you combine two objects into one, using Streams? (Beginning Java forum at Coderanch)
June 3, 2018 - With less code. This class receives the values of it's fields from a webpage. The method puts them in a List, transforms them into Enums, and collects them into 2 lists. Then with the last Stream, it uses the first two lists to create a number of Objects of type Planet.
🌐
HowToDoInJava
howtodoinjava.com › home › java 8 › java 8 stream.concat(): how to combine streams?
Java 8 Stream.concat(): How to Combine Streams?
May 27, 2024 - The Java 8 Stream.concat() method merges two streams into one stream. The combined stream consists of all the elements of both streams.
🌐
Baeldung
baeldung.com › home › java › java collections › java – combine multiple collections
Java - Combine Multiple Collections | Baeldung
May 11, 2024 - It is important to note that Java 8 Streams are not reusable, so you should take this into consideration when assigning them to variables. The flatMap() method returns a Stream after replacing each element of this Stream with the contents of a mapped Stream that is produced by applying the provided mapping function to each element. The example below demonstrates merging of collections using the flatMap() method.
🌐
Baeldung
baeldung.com › home › java › core java › merging java.util.properties objects
Merging java.util.Properties Objects | Baeldung
July 25, 2025 - The first argument is a Supplier function used to create a new result container which in our case is a new Properties object. The Stream API was introduced in Java 8, we have a guide on getting started with this API. In this brief tutorial, we covered three different ways to approach merging two or more Properties objects.
Find elsewhere
🌐
Baeldung
baeldung.com › home › java › java collections › combining different types of collections in java
Combining Different Types of Collections in Java | Baeldung
April 4, 2025 - To learn about the Collectors in detail, visit Guide to Java 8’s Collectors. ... List<Object> combined = Stream.of(first, second).flatMap(Collection::stream).collect(Collectors.toList()); First, we’re using Stream.of() which returns a sequential stream of two lists – first and second. We’ll then pass it to flatMap which will return the contents of a mapped stream after applying the mapping function. This method also discussed in Merging Streams in Java article.
🌐
LabEx
labex.io › tutorials › java-how-to-combine-multiple-streams-efficiently-462121
Java - How to combine multiple streams efficiently
This comprehensive tutorial explores advanced techniques for merging streams, providing developers with practical strategies to handle complex data transformations and improve computational efficiency. Java Streams provide a powerful way to process collections of objects, offering a declarative ...
🌐
Coderwall
coderwall.com › p › im4lja › joining-objects-into-a-string-with-java-8-stream-api
Joining Objects into a String with Java 8 Stream API (Example)
July 17, 2023 - #stream · You can leverage Java 8 Collectors to concatenate some objects into a string separated by a delimiter: For example: List<Integer> numbers = Arrays.asList( 4, 8, 15, 16, 23, 42 ); return numbers.stream() .map( n -> n.toString() ) .collect( ...
🌐
Java Guides
javaguides.net › 2024 › 09 › how-to-merge-two-arrays-using-stream-in-java-8.html
How to Merge Two Arrays Using Stream in Java 8
September 9, 2024 - Define Two Input Arrays: Provide two arrays that need to be merged. Use Java 8 Streams: Use Stream.concat() to merge the two arrays into one stream.
Top answer
1 of 1
4

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.

🌐
Medium
medium.com › @AlexanderObregon › javas-stream-concat-method-explained-4b5fab338fa2
Java’s Stream.concat() Method Explained | Medium
February 3, 2025 - The Stream.concat() method is used to merge two separate streams into a single stream. It does not modify the original streams but creates a new stream that processes elements from both input streams in sequence.
Top answer
1 of 4
4

Assuming class A has a copy constructor that effectively copies the List<B> list attribute and a method that merges two instances of A:

public A(A another) {
    this.id = another.id;
    this.name = another.name;
    this.list = new ArrayList<>(another.list);
}

public A merge(A another) {
    list.addAll(another.list):
    return this;
}

You could achieve what you want as follows:

Map<Integer, A> result = listOfA.stream()
    .collect(Collectors.toMap(A::getId, A::new, A::merge));

Collection<A> result = map.values();

This uses Collectors.toMap, which expects a function that extracts the key of the map from the elements of the stream (here this would be A::getId, which extracts the id of A), a function that transforms each element of the stream to the values of the map (here it would be A::new, which references the copy constructor) and a merge function that combines two values of the map that have the same key (here this would be A::merge, which is only called when the map already contains an entry for the same key).

If you need a List<A> instead of a Collection<A>, simply do:

List<A> result = new ArrayList<>(map.values());
2 of 4
3

If you don't want to use extra functions you can do the following, it's readable and easy to understand, first group by id, create a new object with the first element in the list and then join all the B's classes to finally collect the A's.

List<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId))
    .values().stream()
    .map(grouped -> new A(grouped.get(0).getId(), grouped.get(0).getName(),
            grouped.stream().map(A::getList).flatMap(List::stream)
                .collect(Collectors.toList())))
    .collect(Collectors.toList());

Another way is to use a binary operator and the Collectors.groupingBy method. Here you use the java 8 optional class to create the new A the first time when fst is null.

BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
    .map(cur -> { cur.getList().addAll(snd.getList()); return cur; })
    .orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));

Collection<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId, Collectors.reducing(null, joiner)))
    .values();

If you don't like to use return in short lambdas (doesn't look that well) the only option is a filter because java does not provide another method like stream's peek (note: some IDEs highlight to 'simplify' the expression and mutations shouldn't be made in filter [but i think in maps neither]).

BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
    .filter(cur -> cur.getList().addAll(snd.getList()) || true)
    .orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));

You can also use this joiner as a generic method and create a left to right reducer with a consumer that allows to join the new mutable object created with the initializer function.

public class Reducer {
    public static <A> Collector<A, ?, A> reduce(Function<A, A> initializer, 
                                                BiConsumer<A, A> combiner) {
        return Collectors.reducing(null, (fst, snd) -> Optional.ofNullable(fst)
            .map(cur -> { combiner.accept(cur, snd); return cur; })
            .orElseGet(() -> initializer.apply(snd)));
    }
    public static <A> Collector<A, ?, A> reduce(Supplier<A> supplier, 
                                                BiConsumer<A, A> combiner) {
        return reduce((ign) -> supplier.get(), combiner);
    }
}

And use it like

Collection<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId, Reducer.reduce(
        (cur) -> new A(cur.getId(), cur.getName(), new ArrayList<>(cur.getList())),
        (fst, snd) -> fst.getList().addAll(snd.getList())
    ))).values();

Or like if you have an empty constructor that initializes the collections

Collection<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId, Reducer.reduce(A::new,
        (fst, snd) -> {
            fst.getList().addAll(snd.getList());
            fst.setId(snd.getId());
            fst.setName(snd.getName());
        }
    ))).values();

Finally, if you already have the copy constructor or the merge method mentioned in the other answers you can simplify the code even more or use the Collectors.toMap method.

Top answer
1 of 3
2

Finite sequence

Supposed you want to create a finite sequence of characters containing Y or R, randomly, just create some next() method and call it repeatedly:

public final class SequenceGenerator {
    private final String[] elements;
    private final Random rnd;

    public SequenceGenerator(Collection<String> elements) {
        this.elements = elements.toArray(new String[elements.size()]);
        rnd = new Random();
    }

    public String next() {
        return elements[rnd.nextInt(elements.length)];
    }

    public String generate(int amountOfElements) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < amountOfElements; i++) {
            sb.append(next());
        }
        return sb.toString();
    }
}

The class takes the collection of possible elements to draw from and its next methods chooses one randomly. The generate method calls it repeatedly and appends the drawn elements.

Usage is simple:

SequenceGenerator gen = new SequenceGenerator(List.of("Y", "R"));
System.out.println(gen.generate(11));

May print something like:

YYRYYRYYRYY

Endless stream

You can also nicely use the Stream API here to create an endless stream:

SequenceGenerator gen = new SequenceGenerator(List.of("Y", "R"));
Stream<String> sequence = Stream.generate(gen::next);

Edit: YY and R

As you said you want the stream to start and end with YY, as well as have a R at every third position, we need to modify next() a bit. First of all we add a field int amount to keep track of calls. Then next() is modified to account for YY at the start and R. Likewise generate must be modified to account for YY at the end. The Stream-variant can remain unchanged since it uses the modified next() and doesn't need to account for YY at the end since it is endless.

public final class SequenceGenerator {
    // ...
    private int amount;

    public SequenceGenerator(Collection<String> elements) {
        // ...
        amount = 0;
    }

    public String next() {
        // Start must be 'YY'
        if (amount == 0 || amount == 1) {
            amount++;
            return "Y";
        }

        // Every third must be 'R'
        if (amount % 3 == 0) {
            amount++;
            return "R";
        }

        // Pick a random element
        amount++;
        return elements[rnd.nextInt(elements.length)];
    }

    public String generate(int amountOfElements) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < amountOfElements - 2; i++) {
            sb.append(next());
        }

        // End must be 'YY'
        sb.append("YY");

        return sb.toString();
    }
}

Actually there is another requirement we need to account for. The concatenation of YY at the end must be valid in regards to the rule that forces a R at each third position. So for generate the amountOfElements must be two bigger than a value which is dividable by 3, else the resulting value is invalid.

And we need to account for edge cases, amountOfElements must be at least 5, every smaller sequence is not valid. YYRYY is the first valid sequence. The modified method:

public String generate(int amountOfElements) {
    if (amountOfElements < 5 || (amountOfElements - 2) % 3 != 0) {
        throw new IllegalArgumentException("Invalid sequence length");
    }
    // ...
}
2 of 3
1

If yor want to use a stream, you have to do something like this:

final long iterateLimit = 20;
final String sequence = "YYR";
final String suffix = "YY";
final String yyrString = Stream.generate(() -> sequence)
                               .limit(iterateLimit)
                               .collect(Collectors.joining())
                               .concat(suffix);

I offer generate sequence of "YYR" and add suffix "YY" after creating the string.

🌐
JDriven Blog
blog.jdriven.com › 2021 › 02 › Java-Joy-Merge-Maps-Using-Stream-API
Java Joy: Merge Maps Using Stream API - JDriven Blog
February 26, 2021 - In the following example we use the Stream API to merge multiple Map instances into a new Map using a remapping function for duplicate keys: package com.mrhaki.sample; import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; public class MapMerge { public static void main(String[] args) { Map<Character, Integer> first = Map.of('a', 2, 'b', 3, 'c', 4); Map<Character, Integer> second = Map.of('a', 10, 'c', 11); Map<Character, Integer> third = Map.of('a', 3, 'd', 100); // First we turn multiple maps into a stream of entries and // in the collect method we create a new map and define // a function to multiply the entry value when there is a // duplicate entry key.