Collectors.toMap()
Turns a stream into a Map with a key and value function, and shows how to handle duplicate keys, null values and the map type.
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)- Added in Java Java 8
Parameters
| Parameter | Type | Description |
|---|---|---|
$keyMapper | Function<? super T, ? extends K> | Computes the key for each element. |
$valueMapper | Function<? super T, ? extends U> | Computes the value for each element. It must not return null. |
$mergeFunctionoptional | BinaryOperator<U> | Decides the value when two elements produce the same key: receives the existing value and the new one. Without it a duplicate key throws. |
$mapFactoryoptional | Supplier<M> | Creates the empty map to fill, such as TreeMap::new or LinkedHashMap::new. Only accepted together with a merge function. |
Return value
Collector<T, ?, Map<K, U>>A collector that puts one entry per element into a Map. With two arguments the map type is unspecified (a HashMap in current JDKs); the four-argument form uses the map your factory supplies.
Errors
A null mapper, merge function or factory is not rejected when the collector is built; it fails with NullPointerException once collect runs. The collect operation throws IllegalStateException when two elements map to the same key and no merge function was given, and NullPointerException when the value mapper returns null.
Changelog
- PHP Java 9The IllegalStateException for a duplicate key now names the key and both values. Java 8 put the existing value in the message where the key should have been.
Collectors.toMap builds a Map from a stream. You give it two functions: one that computes the key for each element and one that computes the value. It is the usual way to index a list of rows by id before you look them up, for example after loading users and orders in two queries and joining them in memory.
Map<Long, User> byId = users.stream()
.collect(Collectors.toMap(User::id, Function.identity()));Duplicate keys throw
The two-argument form assumes every key is unique. When two elements produce the same key, the collect call throws IllegalStateException: Duplicate key .... This is the most common surprise with toMap: code that worked in tests fails in production the first time two rows share an email address or a SKU. If duplicates are possible, pass a third argument, a merge function that receives the existing value and the new one and returns the value to keep: (a, b) -> a keeps the first, (a, b) -> b keeps the last, Integer::sum adds them up. If you actually want every element per key, you wanted groupingBy, not toMap.
Null values throw too
A value mapper that returns null causes a NullPointerException, even though HashMap itself accepts null values. An optional column such as a nickname or a middle name is the typical cause. Map nulls to a default value, filter those elements out, or fill a map yourself with the three-argument Stream.collect.
Which map you get
The documentation promises nothing about the map type, mutability or ordering. Today it is a HashMap, so the order of keys in a JSON response or a log line is effectively arbitrary. The four-argument form takes a map factory: LinkedHashMap::new keeps the stream's order, TreeMap::new sorts by key. The factory is only available together with a merge function, so you write one even when you expect no duplicates.
For a map that must not be changed afterwards, Java 10 added Collectors.toUnmodifiableMap. It has the same duplicate-key rule and also rejects null keys.
Example
When to use it
- Indexing users loaded by one query by id so that rows from a second query can be joined to them in memory.
- Turning a list of feature-flag records from the database into a name-to-enabled lookup map at startup.
- Merging cart lines that arrive with the same SKU into one quantity before pricing an order.
Frequently asked questions
How do I fix 'IllegalStateException: Duplicate key' in Collectors.toMap?
Two elements produced the same key. Pass a merge function as the third argument to choose the value to keep, for example (a, b) -> a for the first or (a, b) -> b for the last. If you need every element per key, use Collectors.groupingBy instead.
Why does Collectors.toMap throw NullPointerException?
The value mapper returned null for some element. toMap does not accept null values even though HashMap does. Replace nulls with a default, filter them out, or build the map with stream.collect(HashMap::new, (m, x) -> m.put(...), Map::putAll).
What Map type does Collectors.toMap return?
The two- and three-argument forms make no promise; current JDKs return a HashMap, so key order is not stable. Use the four-argument form with LinkedHashMap::new or TreeMap::new when order matters, or toUnmodifiableMap (Java 10) for a read-only map.
More examples
Null values and a way around them
toMap rejects null values even though the map it builds would accept them. The three-argument Stream.collect calls Map.put directly, so it keeps nulls when your API really needs to tell 'missing' from 'empty'.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
record User(int id, String nickname) {}
void main() {
List<User> users = List.of(new User(1, null), new User(2, "bobby"));
try {
users.stream().collect(Collectors.toMap(User::id, User::nickname));
} catch (NullPointerException e) {
IO.println("toMap failed: " + e.getClass().getSimpleName());
}
// Option 1: substitute a default value.
Map<Integer, String> withDefault = users.stream()
.collect(Collectors.toMap(User::id,
u -> u.nickname() == null ? "" : u.nickname(), (a, b) -> a, TreeMap::new));
IO.println(withDefault);
// Option 2: fill the map yourself; Map.put accepts null values.
Map<Integer, String> withNulls = users.stream()
.collect(TreeMap::new, (map, u) -> map.put(u.id(), u.nickname()), Map::putAll);
IO.println(withNulls);
}
// Output:
// toMap failed: NullPointerException
// {1=, 2=bobby}
// {1=null, 2=bobby}Adding up values for repeated keys
Integer::sum as the merge function turns duplicate keys into a running total, which is how a cart that received the same SKU twice collapses into one line. groupingBy with summingInt gives the same result and reads better when the totals are the point.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
record CartLine(String sku, int qty) {}
void main() {
List<CartLine> lines = List.of(
new CartLine("MUG-1", 2),
new CartLine("TEE-M", 1),
new CartLine("MUG-1", 3));
Map<String, Integer> qtyBySku = lines.stream()
.collect(Collectors.toMap(CartLine::sku, CartLine::qty, Integer::sum, TreeMap::new));
IO.println(qtyBySku);
}
// Output:
// {MUG-1=5, TEE-M=1}Keeping order for a JSON response
Sorting first is wasted if the map does not remember order, so LinkedHashMap::new is passed as the factory. The merge function that throws keeps the strict 'no duplicates' behaviour of the two-argument form while still allowing a factory.
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
record Product(String slug, String name, int sales) {}
void main() {
List<Product> products = List.of(
new Product("mug", "Coffee mug", 40),
new Product("tee", "T-shirt", 95),
new Product("cap", "Cap", 60));
Map<String, Integer> bestSellers = products.stream()
.sorted(Comparator.comparingInt(Product::sales).reversed())
.collect(Collectors.toMap(Product::name, Product::sales,
(a, b) -> { throw new IllegalStateException("duplicate name"); },
LinkedHashMap::new));
IO.println(bestSellers);
}
// Output:
// {T-shirt=95, Cap=60, Coffee mug=40}
Discussion