Handling Duplicate Keys and Null Values with Java Stream toMap: A Practical Guide
This article explains how to convert a list of Java User objects into a Map using Stream's toMap collector, handling duplicate keys and null values with merge functions and Optional, and compares alternative approaches including manual HashMap population.
In JDK 8, Java introduced the powerful Stream API, which has become indispensable for everyday development.
After processing a stream, developers often collect results into a List or Set using collect(Collectors.toList()) or collect(Collectors.toSet()) , and may assume that collect(Collectors.toMap(...)) is equally straightforward.
However, overlooking duplicate keys can turn this assumption into a nightmare.
First, define a simple User entity class:
@Data
@AllArgsConstructor
public class User {
private int id;
private String name;
}Suppose we read a List<User> from a database and want to transform it into a Map where the key is the user's id and the value is the user's name .
A naïve implementation using Collectors.toMap(User::getId, User::getName) compiles but throws IllegalStateException at runtime because the list contains duplicate ids:
public class UserTest {
@Test
public void demo() {
List
userList = new ArrayList<>();
// mock data
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));
Map
map = userList.stream()
.collect(Collectors.toMap(User::getId, User::getName));
System.out.println(map);
}
}The exception occurs because the default toMap collector does not know how to merge values for duplicate keys; we must supply a merge function.
Adding a merge function resolves the duplicate‑key issue, but if any name is null the collector throws a NullPointerException :
public class UserTest {
@Test
public void demo() {
List
userList = new ArrayList<>();
// mock data
userList.add(new User(1, "Alex"));
userList.add(new User(2, null));
Map
map = userList.stream()
.collect(Collectors.toMap(User::getId, User::getName, (oldData, newData) -> newData));
System.out.println(map);
}
}To guard against null names, we can wrap the value extraction with Optional.ofNullable(...).orElse("") while still providing a merge function, resulting in a concise and robust solution:
public class UserTest {
@Test
public void demo() {
List
userList = new ArrayList<>();
// mock data
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));
userList.add(new User(2, null));
Map
map = userList.stream()
.collect(Collectors.toMap(
User::getId,
it -> Optional.ofNullable(it.getName()).orElse(""),
(oldData, newData) -> newData
));
System.out.println(map);
}
}If you prefer an explicit approach, you can iterate over the list with forEach and put entries into a HashMap manually:
public class UserTest {
@Test
public void demo() {
List
userList = new ArrayList<>();
// mock data
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));
userList.add(new User(2, null));
Map
map = new HashMap<>();
userList.forEach(it -> {
map.put(it.getId(), it.getName());
});
System.out.println(map);
}
}The final version demonstrates a clean combination of Stream, Optional, and a merge function to produce a map without duplicate‑key errors or NPEs.
Join the backend‑focused technical community to share knowledge, job referrals, and industry discussions, while keeping the conversation civil and free of advertisements.
Civilized communication should focus on technical exchange, job referrals, and industry discussion.
Advertisements and private solicitations are prohibited.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.