Java Collectors.toMap Pitfalls: Duplicate Keys, Null Values, and Safe Usage Patterns
This article explores hidden pitfalls in Java's Collectors.toMap including duplicate key exceptions, null value handling limitations, null key behavior, and null list safety, providing practical workarounds like merge functions and Optional-based null handling for robust stream-to-map conversions.
Java's Collectors.toMap offers a concise way to convert a stream into a Map, but it contains several subtle traps that can cause runtime exceptions if not handled properly. This article walks through each pitfall with concrete code examples and demonstrates how to avoid them.
1. Baseline: Manual List-to-Map Conversion
Without streams, converting a List<User> to a Map<Long, String> requires explicit iteration:
public void test0() {
User user1 = new User();
user1.setId(1L);
user1.setName("1");
User user2 = new User();
user2.setId(2L);
user2.setName("2");
List<User> list = Lists.newArrayList();
list.add(user1);
list.add(user2);
Map<Long, String> map = new HashMap<>();
for (User user : list) {
map.put(user.getId(), user.getName());
}
}2. Using Collectors.toMap — and the Duplicate Key Trap
The stream version is much shorter:
Map<Long, String> map = list.stream()
.collect(Collectors.toMap(User::getId, User::getName));However, if two elements produce the same key (e.g., both users have id = 1L), the collector throws java.lang.IllegalStateException: Duplicate key 1. This happens because the default implementation does not allow duplicate keys.
3. Handling Duplicate Keys with a Merge Function
The three-argument overload of toMap accepts a merge function that decides which value to keep when a key collision occurs. For example, to keep the first value:
Map<Long, String> map = list.stream()
.collect(Collectors.toMap(User::getId, User::getName, (x1, x2) -> x1));You can also keep the second value ( (x1, x2) -> x2) or implement custom logic such as concatenation or comparison.
4. The Null Value Pitfall
Even with a merge function, Collectors.toMap rejects null values. Setting user2.setName(null) causes a NullPointerException because the internal implementation calls map.merge, which forbids null values.
Workaround: map the value to a non-null default using Optional:
Map<Long, String> map = list.stream()
.collect(Collectors.toMap(
User::getId,
value -> Optional.ofNullable(value.getName()).orElse("")
));Note that this changes the stored value (empty string instead of null), which may not be acceptable for all use cases. An alternative is to filter out null-valued elements beforehand:
list.stream()
.filter(u -> u.getName() != null)
.collect(Collectors.toMap(User::getId, User::getName));5. Null Keys Are Allowed
Unlike values, null keys work without error. The following test passes and produces a map containing a null key:
public void test5() {
User user1 = new User();
user1.setId(null);
user1.setName("1");
User user2 = new User();
user2.setId(2L);
user2.setName("2");
List<User> list = Lists.newArrayList();
list.add(user1);
list.add(user2);
Map<Long, String> map = list.stream()
.collect(Collectors.toMap(User::getId, User::getName));
System.out.print(map); // prints {null=1, 2=2}
}6. Null List vs. Empty List
If the source list reference is null, calling list.stream() throws NullPointerException.
If the list is initialized but empty ( new ArrayList<>()), the stream pipeline completes normally and returns an empty map.
Summary Checklist
Before calling stream(), verify the container itself is not null.
If key uniqueness is not guaranteed, provide a merge function to toMap(keyMapper, valueMapper, mergeFunction).
If values can be null, either filter them out or map them to a non-null default (e.g., via Optional.orElse).
When in doubt, a simple for loop avoids all these edge cases.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.
