Java Collectors Class: Methods with Examples - Quiz

Total: 7 questions

1. 

How does Collectors.partitioningBy() differ from Collectors.groupingBy()?

groupingBy() takes a classifier function and creates as many keys as there are distinct values it returned: a group that never occurred simply has no key in the map. partitioningBy() takes a predicate and always returns a Map<Boolean, ...> with exactly two keys, false and true, even when one half is empty. Hence the practical difference: partitioned.get(true) gives an empty list, while grouped.get(true) gives null and the next method call on it throws NullPointerException. With a downstream collector the empty half gets that collector's "zero" value: partitioningBy(n -> n % 2 == 0, counting()) over [1, 3, 5] yields {false=3, true=0}. So for a binary condition partitioningBy() is the safer tool: no null checks and no getOrDefault() calls.

2. 

What should you do when Collectors.toMap() throws IllegalStateException: Duplicate key, and what is its fourth argument for?

The two-argument toMap(keyMapper, valueMapper) fails on the very first duplicate key. The fix is the third argument, a merge function: it receives the old and the new value and returns the one that stays in the map. The usual choices are (oldValue, newValue) -> oldValue (keep the first), (oldValue, newValue) -> newValue (overwrite with the last) and Integer::sum (add them up). For example, toMap(Employee::department, Employee::salary, Integer::sum) computes the payroll per department. The fourth argument is a Map factory: TreeMap::new sorts the keys, LinkedHashMap::new preserves encounter order, and EnumMap fits enum keys. If you need every value sharing a key rather than a single one, use groupingBy() instead of toMap(). One more trap: toMap() cannot store a null value — internally it calls Map.merge(), which throws NullPointerException.

3. 

In what order do the keys of a groupingBy() result appear, and how do you make that order predictable?

Without an explicit factory groupingBy() collects into a HashMap, whose iteration order follows key hashes and relates neither to the stream order nor to sorting. That is why Stream.of(30, 10, 20, 10, 50).collect(groupingBy(n -> n / 10)) prints {1=[10, 10], 2=[20], 3=[30], 5=[50]} even though key 3 came first. Predictability comes from the three-argument form groupingBy(classifier, mapFactory, downstream): TreeMap::new sorts the keys in natural order, LinkedHashMap::new keeps the order of first appearance in the stream. Calling sorted() on the stream before grouping is pointless — a HashMap reorders the keys anyway. The lists inside the groups, on the other hand, always keep the source order: only the keys get shuffled.

4. 

What are the three forms of Collectors.joining() and what are its limitations?

joining() with no arguments glues the elements together; joining(delimiter) inserts a separator; joining(delimiter, prefix, suffix) also wraps the result. For instance, joining(", ", "[", "]") over names gives [Anna, Ben, Clara], and joining(", ", "WHERE department IN (", ")") builds a ready-made SQL fragment. There is one important limitation: the collector only accepts a stream of CharSequence, so objects must be converted first with map(Employee::name) or map(Object::toString) — otherwise the code will not compile. Internally it uses a StringBuilder, so no pile of intermediate strings appears the way it does with a naive reduce("", String::concat). On an empty stream the three-argument form returns []: the prefix and suffix are always added.

5. 

Why do counting(), summingInt(), averagingInt() and summarizingInt() exist if a stream already has count(), sum() and average()?

Their real home is the downstream position inside groupingBy() or partitioningBy(), where stream methods are not available: groupingBy(Employee::department, counting()) gives the headcount per department, and groupingBy(Employee::department, summingInt(Employee::salary)) gives the payroll. summarizingInt() returns an IntSummaryStatistics with count, sum, min, max and average in a single pass. Things to remember: counting() returns a Long, so declaring Map<String, Integer> with it will not compile; summingInt() accumulates in an int and overflows silently — use summingLong() for money; the averaging* collectors always return a Double and give 0.0 on an empty stream instead of an empty Optional, so "there was no data" and "the average is zero" are indistinguishable from the result.

6. 

What does Collectors.teeing() do and when is it indispensable?

teeing() arrived in Java 12 and takes three arguments: two collectors and a merger function that receives both of their results. It computes two different aggregates over the same data in a single pass: teeing(counting(), summingInt(Employee::salary), Payroll::new) returns Payroll[headcount=5, total=750000], and teeing(minBy(cmp), maxBy(cmp), (min, max) -> ...) gives the minimum and the maximum at once. That matters a great deal when the source is one-shot — a file, a network response, a query result — and a second pass is physically impossible, since a stream cannot be reused. teeing() also nests into groupingBy() as a downstream collector, producing a pair of aggregates per group.

7. 

How do you write a custom collector with Collector.of(), and why is a bug in it so easy to miss?

You do not have to implement the Collector interface in a separate class — the static factory Collector.of() with four functions is enough: supplier creates an empty accumulation container, accumulator adds the next element to it, combiner merges two containers, finisher turns the container into the final result. In Collector<T, A, R> the letter T is the stream element type, A the intermediate container type and R the result type; the container must be mutable, which is why a product collector uses a one-element long[] instead of a Long. When the container and the result share a type, no finisher is needed — there is a three-function overload. The main trap: in a sequential stream the combiner is never called, so a bug hiding there can stay invisible for years and surface the day someone adds .parallel(). Test your collector against parallelStream() as well. For a one-off accumulation the three-argument collect(ArrayList::new, ArrayList::add, ArrayList::addAll) is simpler.

Page 1 of 1