Collecting Stream Elements into Maps

Author      Ter-Petrosyan Hakob

When you work with Java Streams, you often want to turn a stream of objects into a map — a structure that lets you find values by a key. The Collectors.toMap() method is a powerful way to do this.

Imagine you have a Stream<Student> and you want a map where the key is each student’s ID and the value is the student’s name.

public record Student(int id, String name) {}

Map<Integer, String> idToName = students
   .collect(Collectors.toMap(Student::id, Student::name));

The first function (Student::id) creates the map’s keys. The second function (Student::name) creates the map’s values.

If you want the whole object as the value (not just one field), use Function.identity() — it simply returns the same object.

Map<Integer, Student> idToStudent = students
   .collect( Collectors.toMap(Student::id, Function.identity()));

Now each ID maps to its full Student record.

Handling Duplicate Keys

If two elements have the same key, Java doesn’t know which one to keep — so it throws an IllegalStateException. To fix this, you can add a merge function (a third argument) to decide what happens when keys repeat.

Example 1: Suppose two students have the same ID (a mistake in data). You can choose to keep the first one, the last one, or merge them.

Map<Integer, Student> idToStudent = students.collect(
    Collectors.toMap(
        Student::id,
        Function.identity(),
        (existing, replacement) -> existing // keep the first one
    )
);

You could also merge their names or scores if that makes sense for your case.

Example 2: Let’s say you have a list of products. You want a map where the key is the category, and the value is a set of product names in that category.

record Product(String category, String name) {}

Map<String, Set<String>> productsByCategory = products.collect(
    Collectors.toMap(
        Product::category,
        p -> new HashSet<>(Set.of(p.name())),
        (set1, set2) -> {
            set1.addAll(set2);
            return set1;
        }
    )
);

If a category appears several times, the merge function combines the sets — so you end up with all products in that category.

Choosing the Map Type

By default, toMap() uses a HashMap, but you can choose another type — for example, a TreeMap, which keeps keys sorted.

Map<Integer, Student> sortedStudents = students.collect(
    Collectors.toMap(
        Student::id,
        Function.identity(),
        (a, b) -> a,
        TreeMap::new
    )
);

Now your map is automatically sorted by student ID.

Concurrent Maps

When you process a parallel stream, multiple threads work at the same time. To collect results safely and efficiently, use Collectors.toConcurrentMap() — it produces a thread-safe ConcurrentHashMap.

ConcurrentMap<Integer, Student> concurrentMap = students.parallelStream().collect(
    Collectors.toConcurrentMap(Student::id, Function.identity())
);

This avoids merging multiple maps at the end and is faster for large data sets.


java.util.stream.Collectors

These methods return a collector that creates a map, an unmodifiable map, or a concurrent map. The keyMapper and valueMapper functions are used for each element in the stream to produce a key and a value for the map.

If two elements have the same key, Java throws an IllegalStateException by default. You can avoid this error by adding a mergeFunction, which decides how to combine the two values that share the same key.

Normally, the result is a HashMap or ConcurrentHashMap, but you can also give a mapSupplier to create the specific type of map you want.

static <T,K,U> Collector<T,?,Map<K,U>> toMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper)

static <T,K,U> Collector<T,?,Map<K,U>> toMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper, 
   BinaryOperator<U> mergeFunction)

static <T,K,U,M extends Map<K,U>> Collector<T,?,M> toMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper, 
   BinaryOperator<U> mergeFunction, 
   Supplier<M> mapSupplier)

static <T,K,U> Collector<T,?,Map<K,U>> toUnmodifiableMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper) 

static <T,K,U> Collector<T,?,Map<K,U>> toUnmodifiableMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper, 
   BinaryOperator<U> mergeFunction) 10

static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper)

static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper, 
   BinaryOperator<U> mergeFunction)

static <T,K,U,M extends ConcurrentMap<K,U>> Collector<T,?,M> toConcurrentMap(
   Function<? super T,? extends K> keyMapper, 
   Function<? super T,? extends U> valueMapper,
   BinaryOperator<U> mergeFunction, 
   Supplier<M> mapSupplier)