Either is fine. Pick the one that seems more readable to you. If the calculation naturally decomposes, as this one does, then the multiple maps is probably more readable. Some calculations won't naturally decompose, in which case you're stuck at the former. In neither case should you be worrying that one is significantly more performant than the other; that's largely a non-consideration.

Answer from Brian Goetz on Stack Overflow
🌐
Medium
medium.com › @randhirray › create-a-map-from-java-8-stream-with-multiple-different-key-value-pairs-24cec2edc90f
Create a Map from Java 8 stream with multiple different key value pairs | by Randhir Ray | Medium
November 15, 2019 - Map<String, User> map1 = stream.collect(Collectors.toMap(user -> user.getDisplayName(), Function.identity()));System.out.println(map1); Well something bad happened just now. Our code throws an exception instead of printing the map.
Top answer
1 of 2
2

That depends on how you define better. Better might be more CPU or memory efficient, or it could be fewer bytes in the compiled code, or better parallelization.

But I am going to take a chance on assuming that this code runs on a reasonably normal platform and has no special requirements, and therefore being better generally means better readability.

Readability Improvements

In which case, your code is fine. But we can make it easier to read by splitting it into named steps and wrapping it in a good function.

public List<Integer> getNumbersOccuringOddTimes(final List<Integer> numbers) {
    final Map<Integer, Integer> numberCounts = numbers.stream().collect(groupingBy(Function.identity(), counting()));
    final Predicate<Entry> isOdd = e -> e.getValue() % 2 == 1;
    return numberCounts.entrySet().stream().filter(isOdd).map(Entry::getKey).collect(toList());
}

I've made five minor adjustments to your code to help with readability. I have;

  1. Added a descriptive method name that wraps the steps.
  2. Split the two streams into separate statements.
  3. Used static imports (not shown) for the Collectors functions.
  4. Moved the modulus check into a predicate variable (isOdd) so it can be named.
  5. Swapped out the lambda in .map to a method reference because it is shorter and more descriptive.
2 of 2
1

As Rudi Kershaw said, there isn't much to fix technically. Whatever your way of implementation is, you need to count the occurences, select the odd ones and collect the values. Streams are not inherently readable so what you can do is to concentrate on the presentation a bit more. The variable names you use are either straight out confusing or just not very descriptive.

For example, you're processing Map.Entry objects, not Map objects, when filtering out the odd ones. Thus use entry in the lambda instead of map:

.filter(entry -> entry.getValue() % 2 == 1)

Likewise when picking the keys from the entries, use a variable name that describes the object being processed:

.map(entry -> entry.getKey())

With those changes and some formatting, your code becomes this. For someone who is familiar with common stream concepts, this should be pretty clear.

List<Integer> oddOcuranceNumbers = numbers.stream()
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
    .entrySet().stream()
    .filter(entry -> entry.getValue() % 2 == 1)
    .map(entry -> entry.getKey())
    .collect(Collectors.toList());
🌐
Stack Abuse
stackabuse.com › java-8-stream-map-examples
Java 8 - Stream.map() Examples
February 23, 2023 - This is why we can chain multiple intermediate operations without using semicolons. Intermediate operations include the following methods: map(op) - Returns a new stream in which the provided op function is applied to each of the elements in the original stream
🌐
Novixys Software
novixys.com › blog › java-8-streams-map-examples
Java 8 Streams Map Examples | Novixys Software Dev Blog
January 5, 2018 - What formerly required multiple lines of code and loops can now be accomplished with a single line of code. A part of Java 8 streams is the map() facility which can map one value to another in a stream.
Find elsewhere
🌐
Oracle
docs.oracle.com › javase › 8 › docs › api › java › util › stream › Stream.html
Stream (Java Platform SE 8 )
July 21, 2026 - When executed in parallel, multiple intermediate results may be instantiated, populated, and merged so as to maintain isolation of mutable data structures. Therefore, even when executed in parallel with non-thread-safe data structures (such as ArrayList), no additional synchronization is needed for a parallel reduction. ... Map<String, List<Person>> peopleByCity = personStream.collect(Collectors.groupingBy(Person::getCity));
Top answer
1 of 2
9

The problem comes from the fact that you're using a generic wildcard ?. What you want is to have a parameterized type T, that will represent the type of the Stream element. Assuming the function would return the same type as their input, you could have:

private static <T> List<T> multipleMapping(final Collection<T> collection, final List<Function<T, T>> functions) {
    Stream<T> stream = collection.stream();
    for (Function<T, T> function : functions) {
        stream = stream.map(function);
    }
    return stream.collect(Collectors.toList());
}

This compiles fine: the mapper given to map correcly accepts a T and returns a T. However, if the functions don't return the same type as their input then you won't be able to keep type-safety and will have to resort to using List<Function<Object, Object>>.


Note that we could use a UnaryOperator<T> instead of Function<T, T>.

Also, you could avoid the for loop and reduce all functions into a single one using andThen:

private static <T> List<T> multipleMapping(final Collection<T> collection, final List<Function<T, T>> functions) {
    return collection.stream()
                     .map(functions.stream().reduce(Function.identity(), Function::andThen))
                     .collect(Collectors.toList());
}
2 of 2
4

If you have few functions (i.e. if you can write them down), then I suggest you don't add them to a list. Instead, compose them into a single function, and then apply that single function to each element of the given collection.

Your multipleMapping() method would now receive a single function:

public static <T, R> List<R> multipleMapping(
    Collection<T> collection, Function<T, R> function) {

    return collection.stream()
            .map(function)
            .collect(Collectors.toList());
}

Then, in the calling code, you could create a function composed of many functions (you will have all the functions anyway) and invoke the multipleMapping() method with that function.

For example, suppose we have a list of candidates:

List<String> candidates = Arrays.asList(
        "Hillary", "Donald",
        "Bernie", "Ted", "John");

And four functions:

Function<String, Integer> f1 = String::length;

Function<Integer, Long> f2 = i -> i * 10_000L;

Function<Long, LocalDate> f3 = LocalDate::ofEpochDay;

Function<LocalDate, Integer> f4 = LocalDate::getYear;

These functions can be used to compose a new function, as follows:

Function<String, Integer> function = f1.andThen(f2).andThen(f3).andThen(f4);

Or also this way:

Function<String, Integer> composed = f4.compose(f3).compose(f2).compose(f1);

Now, you can invoke your multipleMapping() method with the list of candidates and the composed function:

List<Integer> scores = multipleMapping(candidates, function);

So we have transformed our list of candidates into a list of scores, by explicitly composing a new function from four different functions and applying this composed function to each candidate.

If you want to know who will win the election, you could check which candidate has the highest score, but I will let that as an exercise for whoever is interested in politics ;)

🌐
JavaBrahman
javabrahman.com › java-8 › java-8-mapping-with-streams-map-flatmap-methods-tutorial-with-examples
Java 8 Mapping with Streams | map and flatMap methods tutorial with examples - JavaBrahman
What is mapping with Streams Mapping in the context of Java 8 Streams refers to converting or transforming a Stream carrying one type of data to another type. Lets take an example. Say we have a stream containing elements of type String. Suppose what we need is a stream of data which contains Integer values with each Integer value of the new stream being the length of corresponding String in the old stream.
🌐
Baeldung
baeldung.com › home › java › java collections › java map › working with maps using streams
Working With Maps Using Streams | Baeldung
November 10, 2025 - We only want to consider the entries with “Effective Java” as the title, so the first intermediate operation will be a filter. We’re not interested in the whole Map entry, but in the key of each entry. So the next chained intermediate operation does just that: it is a map operation that will generate a new stream as output, which will contain only the keys for the entries that matched the title we were looking for.
Top answer
1 of 1
22

I think your alternatives 2 and 3 can be re-written to be more clear:

Alternative 2:

Map<String, Customer> res2 = customers.stream()
    .flatMap(
        c -> Stream.of(c.first, c.last)
        .map(k -> new AbstractMap.SimpleImmutableEntry<>(k, c))
    ).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

Alternative 3: Your code abuses reduce by mutating the HashMap. To do mutable reduction, use collect:

Map<String, Customer> res3 = customers.stream()
    .collect(
        HashMap::new, 
        (m,c) -> {m.put(c.first, c); m.put(c.last, c);}, 
        HashMap::putAll
    );

Note that these are not identical. Alternative 2 will throw an exception if there are duplicate keys while Alternative 3 will silently overwrite the entries.

If overwriting entries in case of duplicate keys is what you want, I would personally prefer Alternative 3. It is immediately clear to me what it does. It most closely resembles the iterative solution. I would expect it to be more performant as Alternative 2 has to do a bunch of allocations per customer with all that flatmapping.

However, Alternative 2 has a huge advantage over Alternative 3 by separating the production of entries from their aggregation. This gives you a great deal of flexibility. For example, if you want to change Alternative 2 to overwrite entries on duplicate keys instead of throwing an exception, you would simply add (a,b) -> b to toMap(...). If you decide you want to collect matching entries into a list, all you would have to do is replace toMap(...) with groupingBy(...), etc.

Top answer
1 of 2
65

It's an interesting question, because it shows that there are a lot of different approaches to achieve the same result. Below I show three different implementations.


Default methods in Collection Framework: Java 8 added some methods to the collections classes, that are not directly related to the Stream API. Using these methods, you can significantly simplify the implementation of the non-stream implementation:

Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
    Map<String, DataSet> result = new HashMap<>();
    multiDataPoints.forEach(pt ->
        pt.keyToData.forEach((key, value) ->
            result.computeIfAbsent(
                key, k -> new DataSet(k, new ArrayList<>()))
            .dataPoints.add(new DataPoint(pt.timestamp, value))));
    return result.values();
}

Stream API with flatten and intermediate data structure: The following implementation is almost identical to the solution provided by Stuart Marks. In contrast to his solution, the following implementation uses an anonymous inner class as intermediate data structure.

Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
    return multiDataPoints.stream()
        .flatMap(mdp -> mdp.keyToData.entrySet().stream().map(e ->
            new Object() {
                String key = e.getKey();
                DataPoint dataPoint = new DataPoint(mdp.timestamp, e.getValue());
            }))
        .collect(
            collectingAndThen(
                groupingBy(t -> t.key, mapping(t -> t.dataPoint, toList())),
                m -> m.entrySet().stream().map(e -> new DataSet(e.getKey(), e.getValue())).collect(toList())));
}

Stream API with map merging: Instead of flattening the original data structures, you can also create a Map for each MultiDataPoint, and then merge all maps into a single map with a reduce operation. The code is a bit simpler than the above solution:

Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
    return multiDataPoints.stream()
        .map(mdp -> mdp.keyToData.entrySet().stream()
            .collect(toMap(e -> e.getKey(), e -> asList(new DataPoint(mdp.timestamp, e.getValue())))))
        .reduce(new HashMap<>(), mapMerger())
        .entrySet().stream()
        .map(e -> new DataSet(e.getKey(), e.getValue()))
        .collect(toList());
}

You can find an implementation of the map merger within the Collectors class. Unfortunately, it is a bit tricky to access it from the outside. Following is an alternative implementation of the map merger:

<K, V> BinaryOperator<Map<K, List<V>>> mapMerger() {
    return (lhs, rhs) -> {
        Map<K, List<V>> result = new HashMap<>();
        lhs.forEach((key, value) -> result.computeIfAbsent(key, k -> new ArrayList<>()).addAll(value));
        rhs.forEach((key, value) -> result.computeIfAbsent(key, k -> new ArrayList<>()).addAll(value));
        return result;
    };
}
2 of 2
14

To do this, I had to come up with an intermediate data structure:

class KeyDataPoint {
    String key;
    DateTime timestamp;
    Number data;
    // obvious constructor and getters
}

With this in place, the approach is to "flatten" each MultiDataPoint into a list of (timestamp, key, data) triples and stream together all such triples from the list of MultiDataPoint.

Then, we apply a groupingBy operation on the string key in order to gather the data for each key together. Note that a simple groupingBy would result in a map from each string key to a list of the corresponding KeyDataPoint triples. We don't want the triples; we want DataPoint instances, which are (timestamp, data) pairs. To do this we apply a "downstream" collector of the groupingBy which is a mapping operation that constructs a new DataPoint by getting the right values from the KeyDataPoint triple. The downstream collector of the mapping operation is simply toList which collects the DataPoint objects of the same group into a list.

Now we have a Map<String, List<DataPoint>> and we want to convert it to a collection of DataSet objects. We simply stream out the map entries and construct DataSet objects, collect them into a list, and return it.

The code ends up looking like this:

Collection<DataSet> convertMultiDataPointToDataSet(List<MultiDataPoint> multiDataPoints) {
    return multiDataPoints.stream()
        .flatMap(mdp -> mdp.getData().entrySet().stream()
                           .map(e -> new KeyDataPoint(e.getKey(), mdp.getTimestamp(), e.getValue())))
        .collect(groupingBy(KeyDataPoint::getKey,
                    mapping(kdp -> new DataPoint(kdp.getTimestamp(), kdp.getData()), toList())))
        .entrySet().stream()
        .map(e -> new DataSet(e.getKey(), e.getValue()))
        .collect(toList());
}

I took some liberties with constructors and getters, but I think they should be obvious.

🌐
Coderwall
coderwall.com › p › oflatw › merging-multiple-maps-using-java-8-streams
Merging Multiple Maps Using Java 8 Streams (Example)
September 27, 2021 - There are multiple ways of doing this but my preferred one is using Stream.concat() and passing the entry sets of all maps as parameters: Stream.concat(visitCounts1.entrySet().stream(), visitCounts2.entrySet().stream()); Then, comes the collecting ...
Top answer
1 of 2
7

Generally collecting to anything other than standard API's gives you is pretty easy via a custom Collector. In your case collecting to 3 lists at a time (just a small example that compiles, since you can't share your code either):

private static <T> Collector<T, ?, List<List<T>>> to3Lists() {
    class Acc {

        List<T> left = new ArrayList<>();

        List<T> middle = new ArrayList<>();

        List<T> right = new ArrayList<>();

        List<List<T>> list = Arrays.asList(left, middle, right);

        void add(T elem) {
            // obviously do whatever you want here
            left.add(elem);
            middle.add(elem);
            right.add(elem);
        }

        Acc merge(Acc other) {

            left.addAll(other.left);
            middle.addAll(other.middle);
            right.addAll(other.right);

            return this;
        }

        public List<List<T>> finisher() {
            return list;
        }

    }
    return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finisher);
}

And using it via:

Stream.of(1, 2, 3)
      .collect(to3Lists());

Obviously this custom collector does not do anything useful, but just an example of how you could work with it.

2 of 2
4

I have adapted the answer to this question to your case. The custom Spliterator will "split" the stream into multiple streams that collect by different properties:

@SafeVarargs
public static <T> long streamForked(Stream<T> source, Consumer<Stream<T>>... consumers)
{
    return StreamSupport.stream(new ForkingSpliterator<>(source, consumers), false).count();
}

public static class ForkingSpliterator<T>
    extends AbstractSpliterator<T>
{
    private Spliterator<T>         sourceSpliterator;

    private List<BlockingQueue<T>> queues = new ArrayList<>();

    private boolean                sourceDone;

    @SafeVarargs
    private ForkingSpliterator(Stream<T> source, Consumer<Stream<T>>... consumers)
    {
        super(Long.MAX_VALUE, 0);

        sourceSpliterator = source.spliterator();

        for (Consumer<Stream<T>> fork : consumers)
        {
            LinkedBlockingQueue<T> queue = new LinkedBlockingQueue<>();
            queues.add(queue);
            new Thread(() -> fork.accept(StreamSupport.stream(new ForkedConsumer(queue), false))).start();
        }
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action)
    {
        sourceDone = !sourceSpliterator.tryAdvance(t -> queues.forEach(queue -> queue.offer(t)));
        return !sourceDone;
    }

    private class ForkedConsumer
        extends AbstractSpliterator<T>
    {
        private BlockingQueue<T> queue;

        private ForkedConsumer(BlockingQueue<T> queue)
        {
            super(Long.MAX_VALUE, 0);
            this.queue = queue;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action)
        {
            while (queue.peek() == null)
            {
                if (sourceDone)
                {
                    // element is null, and there won't be no more, so "terminate" this sub stream
                    return false;
                }
            }

            // push to consumer pipeline
            action.accept(queue.poll());

            return true;
        }
    }
}

You can use it as follows:

streamForked(Stream.of(new Row("content1", "client1", "location1", 1),
                       new Row("content2", "client1", "location1", 2),
                       new Row("content1", "client1", "location2", 3),
                       new Row("content2", "client2", "location2", 4),
                       new Row("content1", "client2", "location2", 5)),
             rows -> System.out.println(rows.collect(Collectors.groupingBy(Row::getClient,
                                                                           Collectors.groupingBy(Row::getContent,
                                                                                                 Collectors.summingInt(Row::getConsumption))))),
             rows -> System.out.println(rows.collect(Collectors.groupingBy(Row::getClient,
                                                                           Collectors.groupingBy(Row::getLocation,
                                                                                                 Collectors.summingInt(Row::getConsumption))))),
             rows -> System.out.println(rows.collect(Collectors.groupingBy(Row::getContent,
                                                                           Collectors.groupingBy(Row::getLocation,
                                                                                                 Collectors.summingInt(Row::getConsumption))))));

// Output
// {client2={location2=9}, client1={location1=3, location2=3}}
// {client2={content2=4, content1=5}, client1={content2=2, content1=4}}
// {content2={location1=2, location2=4}, content1={location1=1, location2=8}}

Note that you can do pretty much anything you want with your the copies of the stream. As per your example, I used a stacked groupingBy collector to group the rows by two properties and then summed up the int property. So the result will be a Map<String, Map<String, Integer>>. But you could also use it for other scenarios:

rows -> System.out.println(rows.count())
rows -> rows.forEach(row -> System.out.println(row))
rows -> System.out.println(rows.anyMatch(row -> row.getConsumption() > 3))
🌐
Stackify
stackify.com › streams-guide-java-8
A Guide to Java Streams in Java 8 - Stackify
September 4, 2024 - One of the most important characteristics of Java streams is that they allow for significant optimizations through lazy evaluations. Computation on the source data is only performed when the terminal operation is initiated, and source elements are consumed only as needed. All intermediate operations are lazy, so they’re not executed until a result of processing is needed. For example, consider the findFirst() example we saw earlier. How many times is the map() operation performed here?