Stream API with Collections — toList, toSet, toMap, groupingBy
Fast recall
Quick revision
Core answer
HashSet; `Collectors.toMap(keyFn, valueFn)` — HashMap, throws `IllegalStateException` on duplicate keys unless you provide a merge function: Collectors.toMap(k, v, (a, b) -> a); `Collectors.toUnmodifiableList/Set/Map(...)` — immutable variants (Java 10+); `Collectors.groupingBy(classifier)` — Map> grouped by classifier; `Collectors.groupingBy(classifier, downstream)` — composable: groupingBy(Person::getDept, counting()) gives Map; `Collectors.partitioningBy(predicate)` — Map> (faster than groupingBy with boolean key); `Collectors.counting()`/`summingInt/Long/Double`/`averagingInt/Long/Double` — aggregations as downstream collectors; `Collectors.joining(delimiter)` — concatenate strings. Modern idiom (Java 16+): `stream.toList()` instance method returns an unmodifiable list — shorter and more efficient than collect(Collectors.toList()). Pitfalls: (1) `toMap` throws on duplicate keys by default — always provide a merge function for multi-valued data; (2) `groupingBy` returns `HashMap` — order not preserved; use groupingBy(classifier, LinkedHashMap::new, downstream) for insertion order; (3) mutability differs between Collectors.toList() (mutable) and Stream.toList() (immutable) — confusing migration. For interviews: lead with 'Stream + Collectors replaces manual loops with declarative pipelines; groupingBy and toMap are the workhorses; watch for duplicate-key exceptions in toMap'.Understanding of terminal collectors and their relationship to collection types
Using Collectors.toList() and assuming the result is unmodifiable (it returns a mutable ArrayList), or not knowing about toUnmodifiableList()
Demonstrate groupingBy with downstream collectors, partitioningBy for boolean splits, and the difference between toMap with merge functions vs without
Interview-ready guide
Interview answer
Stream collectors define how a stream's elements are accumulated into a final result — toList(), toMap(), groupingBy(), and joining() are the ones that appear most in production code and carry the most tricky edge cases in interviews.
toMap():
- Throws
IllegalStateExceptionon duplicate keys by default — a runtime crash that only surfaces with real data - Fix: merge function —
toMap(Order::getId, identity(), (a, b) -> b)keeps the last entry - Fourth argument controls Map type:
toMap(k, v, merge, LinkedHashMap::new)for insertion order
groupingBy():
- Stream equivalent of SQL
GROUP BY— downstream collectors replace the default List accumulation groupingBy(Order::getStatus, counting())gives a frequency map in one linegroupingBy(dept, averagingDouble(Employee::getSalary))for aggregates; can nest for multi-level grouping
partitioningBy() vs groupingBy():
partitioningBy()always returns bothtrueandfalsekeys — even if one group is emptygroupingBy()omits empty groups
toList() mutability gotcha:
- Java 16
Stream.toList()→ unmodifiable;Collectors.toList()→ mutable ArrayList — switching breaks code that modifies the result
I recommend always providing a merge function with toMap() and being explicit about whether you want a mutable or unmodifiable result list.
Stream API with Collections — toList, toSet, toMap, groupingBy
Direct Answer
Stream API + Collectors is the modern way to transform streams into collections. Core collectors: Collectors.toList() — collects to a mutable ArrayList (Java 8-15) or unmodifiable list (Stream.toList() in Java 16+); Collectors.toSet() — mutable HashSet; Collectors.toMap(keyFn, valueFn) — HashMap, throws IllegalStateException on duplicate keys unless you provide a merge function: Collectors.toMap(k, v, (a, b) -> a); Collectors.toUnmodifiableList/Set/Map(...) — immutable variants (Java 10+); Collectors.groupingBy(classifier) — Map<K, List<T>> grouped by classifier; Collectors.groupingBy(classifier, downstream) — composable: groupingBy(Person::getDept, counting()) gives Map<Dept, Long>; Collectors.partitioningBy(predicate) — Map<Boolean, List<T>> (faster than groupingBy with boolean key); Collectors.counting()/summingInt/Long/Double/averagingInt/Long/Double — aggregations as downstream collectors; Collectors.joining(delimiter) — concatenate strings. Modern idiom (Java 16+): stream.toList() instance method returns an unmodifiable list — shorter and more efficient than collect(Collectors.toList()). Pitfalls: (1) toMap throws on duplicate keys by default — always provide a merge function for multi-valued data; (2) groupingBy returns HashMap — order not preserved; use groupingBy(classifier, LinkedHashMap::new, downstream) for insertion order; (3) mutability differs between Collectors.toList() (mutable) and Stream.toList() (immutable) — confusing migration. For interviews: lead with 'Stream + Collectors replaces manual loops with declarative pipelines; groupingBy and toMap are the workhorses; watch for duplicate-key exceptions in toMap'.
Deep Dive & Architecture
compute, merge, computeIfAbsent, and computeIfPresent are Map methods (Java 8+) that atomically combine get + check + put into a single operation, replacing the verbose get-check-put anti-pattern. computeIfAbsent(key, mappingFunction) — if key absent, compute value and put; most-used for lazy initialization of multi-valued maps: map.computeIfAbsent(key, k -> new ArrayList<>()).add(value). computeIfPresent(key, remappingFunction) — if key present, recompute value (or remove if null returned). compute(key, remappingFunction) — recompute regardless of presence (function gets old value or null). merge(key, value, remappingFunction) — if absent, put value; if present, apply function to combine: map.merge(word, 1, Integer::sum) is the idiomatic word-counter. Why they matter: (1) atomic in ConcurrentHashMap — no lost updates from concurrent get-check-put races; (2) less code than if (map.containsKey(k)) map.put(k, ...) else map.put(k, ...); (3) cleaner intent — the operation says what it does. Pitfalls: (1) not atomic in non-concurrent maps (HashMap) — only the API is single-call, not thread-safe; (2) returning null from a remapping function removes the key — surprising; (3) don't recursively modify the map inside the function — undefined behavior; (4) merge doesn't handle null values the same way as compute — read the Javadoc. For interviews: lead with 'replaces get-check-put, atomic in ConcurrentHashMap, idiomatic for multimap lazy-init and counters'.
Stream API + Collectors is the modern way to transform streams into collections. Core collectors: Collectors.toList() — collects to a mutable ArrayList (Java 8-15) or unmodifiable list (Stream.toList() in Java 16+); Collectors.toSet() — mutable HashSet; Collectors.toMap(keyFn, valueFn) — HashMap, throws IllegalStateException on duplicate keys unless you provide a merge function: Collectors.toMap(k, v, (a, b) -> a); Collectors.toUnmodifiableList/Set/Map(...) — immutable variants (Java 10+); Collectors.groupingBy(classifier) — Map<K, List<T>> grouped by classifier; Collectors.groupingBy(classifier, downstream) — composable: groupingBy(Person::getDept, counting()) gives Map<Dept, Long>; Collectors.partitioningBy(predicate) — Map<Boolean, List<T>> (faster than groupingBy with boolean key); Collectors.counting()/summingInt/Long/Double/averagingInt/Long/Double — aggregations as downstream collectors; Collectors.joining(delimiter) — concatenate strings. Modern idiom (Java 16+): stream.toList() instance method returns an unmodifiable list — shorter and more efficient than collect(Collectors.toList()). Pitfalls: (1) toMap throws on duplicate keys by default — always provide a merge function for multi-valued data; (2) groupingBy returns HashMap — order not preserved; use groupingBy(classifier, LinkedHashMap::new, downstream) for insertion order; (3) mutability differs between Collectors.toList() (mutable) and Stream.toList() (immutable) — confusing migration. For interviews: lead with 'Stream + Collectors replaces manual loops with declarative pipelines; groupingBy and toMap are the workhorses; watch for duplicate-key exceptions in toMap'.
A common mistake is confusing the interface with the implementation (e.g., saying List when you mean ArrayList), or not knowing that Map does not extend Collection.
Common Mistakes & Pitfalls
Using Collectors.toList() and assuming the result is unmodifiable (it returns a mutable ArrayList), or not knowing about toUnmodifiableList()
Check your understanding
Answer these without looking back at the article.
- What is the Java Collections Framework (JCF) and why does it exist? It provides a unified architecture of interfaces and implementations for storing and manipulating groups of objects, replacing the ad-hoc pre-JCF classes (Vector, Hashtable, Enumeration) with a consistent, interoperable API.
- How does HashSet work internally in Java?
- How does HashMap internally handle hash collisions in Java 8+? Explain the transition from linked list to tree.
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.