Collector vs Reducer in Java Streams
Collectors and Reduction are powerful terminal operations in Java Streams for aggregating data. The reduce() method performs immutable reductions to produce a single result value like sum or maximum, creating new values at each step. The collect() method performs mutable reductions, accumulating elements into containers like List, Map, or Set by mutating existing values. Collectors provide pre-built operations for common tasks like grouping, partitioning, joining, and statistical summaries.
Interview Questions & Expert Answers
Section titled βInterview Questions & Expert Answersβ1. Explain the difference between reduce() and collect(). When would you use each?
Section titled β1. Explain the difference between reduce() and collect(). When would you use each?βYour Answer:
The key difference is mutability :
reduce()- Performs immutable reduction, always creating a new value at each step. Returns a single result valuecollect()- Performs mutable reduction, updating/mutating an existing container. Returns a collection or complex structure
Example showing the difference:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// reduce() - creates new Integer at each step, returns single valueint sum = numbers.stream() .reduce(0, Integer::sum); // Result: 15
// collect() - mutates a List, returns collectionList<Integer> collected = numbers.stream() .collect(Collectors.toList()); // Result: [1, 2, 3, 4, 5]When to use each :
Use reduce() when:
- You need a single result (sum, product, max, min, concatenation)
- The reduction logic is simple and associative
- Working with immutable values
Use collect() when:
- You need to transform stream into a collection (List, Set, Map)
- You need grouping, partitioning, or complex aggregations
- You want to use pre-built Collectors utilities
The reduce() method is more memory efficient for simple aggregations since it doesnβt create intermediate containers.
2. How would you group employees by department and then count them in each department?
Section titled β2. How would you group employees by department and then count them in each department?βYour Answer:
Use Collectors.groupingBy() with a downstream collector counting():
Map<String, Long> employeeCountByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, // Classifier function Collectors.counting() // Downstream collector ));
// Result: {IT=15, HR=8, Finance=12, Sales=20}Breaking it down:
groupingBy(Employee::getDepartment)- Groups employees by their department fieldCollectors.counting()- Counts elements in each group- Result is
Map<String, Long>where key is department, value is count
Alternative without downstream collector (gives list of employees per department):
Map<String, List<Employee>> empsByDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment));// Then get counts: empsByDept.get("IT").size()The first approach is more efficient as it directly computes counts without storing employee objects.
3. Whatβs the difference between Collectors.toList() and Collectors.toCollection()?
Section titled β3. Whatβs the difference between Collectors.toList() and Collectors.toCollection()?βYour Answer:
The difference is in control over the collection type:
Collectors.toList():
- Returns an unspecified List implementation (usually ArrayList)
- No control over which List type is created
- Most commonly used for general cases
List<String> list = stream.collect(Collectors.toList());// Returns some List implementation - you don't know whichCollectors.toCollection():
- Lets you specify exactly which collection type you want
- Takes a Supplier that creates the collection
- Use when you need specific collection implementation
// Create LinkedList specificallyLinkedList<String> linkedList = stream .collect(Collectors.toCollection(LinkedList::new));
// Create TreeSet for sorted unique elementsTreeSet<String> treeSet = stream .collect(Collectors.toCollection(TreeSet::new));
// Create specific ArrayListArrayList<String> arrayList = stream .collect(Collectors.toCollection(ArrayList::new));When to use each:
- Use
toList()when any List implementation is fine (most cases) - Use
toCollection()when you need:- Specific collection type (LinkedList, TreeSet, etc.)
- Thread-safe collections (CopyOnWriteArrayList)
- Custom collection implementations
4. How do you partition a list based on a predicate using Collectors?
Section titled β4. How do you partition a list based on a predicate using Collectors?βYour Answer:
Use Collectors.partitioningBy() which creates a Map<Boolean, List<T>> with two entries - true and false:
List<Employee> employees = getEmployees();
// Partition employees by salary > 50000Map<Boolean, List<Employee>> partitioned = employees.stream() .collect(Collectors.partitioningBy( emp -> emp.getSalary() > 50000 ));
List<Employee> highEarners = partitioned.get(true); // Salary > 50000List<Employee> lowEarners = partitioned.get(false); // Salary <= 50000Key characteristics:
- Always returns a map with exactly 2 keys:
trueandfalse - Both keys exist even if one list is empty
- More efficient than filtering twice
With downstream collector (count instead of list):
Map<Boolean, Long> partitionedCount = employees.stream() .collect(Collectors.partitioningBy( emp -> emp.getSalary() > 50000, Collectors.counting() ));
// Result: {false=45, true=55}Difference from groupingBy:
partitioningBy()- Binary split (2 groups: true/false)groupingBy()- Multiple groups based on key function (0 to N groups)
5. Write a collector operation to find the average salary by department from an employee list.
Section titled β5. Write a collector operation to find the average salary by department from an employee list.βYour Answer:
Use groupingBy() with averagingDouble() downstream collector:
Map<String, Double> avgSalaryByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.averagingDouble(Employee::getSalary) ));
// Result: {IT=75000.0, HR=55000.0, Finance=80000.0}Breaking it down:
groupingBy(Employee::getDepartment)- Groups by departmentaveragingDouble(Employee::getSalary)- Calculates average of salary field- Returns
Map<String, Double>with department as key, average as value
Alternative approaches:
With custom collector for more statistics:
Map<String, DoubleSummaryStatistics> salaryStats = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.summarizingDouble(Employee::getSalary) ));
// Access multiple statsDoubleSummaryStatistics itStats = salaryStats.get("IT");double avgSalary = itStats.getAverage();double maxSalary = itStats.getMax();long count = itStats.getCount();If you need different numeric types:
averagingInt()- For int valuesaveragingLong()- For long valuesaveragingDouble()- For double values (most flexible)
6. Explain Collectors.groupingBy() with downstream collectors. Give a nested grouping example.
Section titled β6. Explain Collectors.groupingBy() with downstream collectors. Give a nested grouping example.βYour Answer:
groupingBy() with downstream collectors performs multi-level aggregations - first grouping, then applying another operation on each group.
Basic syntax:
Collectors.groupingBy( classifierFunction, // How to group downstreamCollector // What to do with each group)Simple downstream example:
// Group by department, then count employeesMap<String, Long> result = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, // Classifier Collectors.counting() // Downstream ));Nested grouping example (multi-level hierarchy):
// Group employees by department, then by job level, then collect namesMap<String, Map<String, List<String>>> nestedGroups = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, // First level: by department Collectors.groupingBy( // Second level: by job level Employee::getJobLevel, Collectors.mapping( // Third level: extract names Employee::getName, Collectors.toList() ) ) ));
// Result structure:// {// "IT": {// "Senior": ["John", "Jane"],// "Junior": ["Bob", "Alice"]// },// "HR": {// "Senior": ["Mike"],// "Junior": ["Sarah", "Tom"]// }// }Complex example with multiple downstream collectors:
// Group by department, find max salary in eachMap<String, Optional<Employee>> highestPaidByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.maxBy(Comparator.comparing(Employee::getSalary)) ));Common downstream collectors:
counting()- Count elementssummingInt/Long/Double()- Sum valuesaveragingInt/Long/Double()- Calculate averagesmaxBy/minBy()- Find max/minmapping()- Transform then collectfiltering()- Filter then collect
7. How would you create a custom collector? What are the five arguments needed?
Section titled β7. How would you create a custom collector? What are the five arguments needed?βYour Answer:
Create a custom collector using Collector.of() with five arguments:
The five components:
- Supplier - Creates the result container
- Accumulator - Adds element to container
- Combiner - Merges two containers (for parallel streams)
- Finisher - Final transformation of container
- Characteristics - Optimization hints (optional)
Example - Custom collector to join strings with brackets:
Collector<String, StringBuilder, String> customCollector = Collector.of( // 1. Supplier: Create container () -> new StringBuilder("["),
// 2. Accumulator: Add element to container (sb, s) -> { if (sb.length() > 1) sb.append(", "); sb.append(s); },
// 3. Combiner: Merge two containers (parallel processing) (sb1, sb2) -> { if (sb1.length() > 1) sb1.append(", "); sb1.append(sb2.substring(1)); return sb1; },
// 4. Finisher: Transform container to final result sb -> sb.append("]").toString(),
// 5. Characteristics: Optimization hints Collector.Characteristics.CONCURRENT);
// UsageString result = Stream.of("A", "B", "C") .collect(customCollector);// Result: "[A, B, C]"Practical example - Custom collector for immutable list:
Collector<String, List<String>, List<String>> toImmutableList = Collector.of( ArrayList::new, // 1. Supplier List::add, // 2. Accumulator (list1, list2) -> { // 3. Combiner list1.addAll(list2); return list1; }, Collections::unmodifiableList // 4. Finisher);When characteristics is omitted, the collector has no special properties. Common characteristics include CONCURRENT, UNORDERED, and IDENTITY_FINISH.
8. Whatβs the difference between summingInt() and reducing() when calculating sums?
Section titled β8. Whatβs the difference between summingInt() and reducing() when calculating sums?βYour Answer:
Both calculate sums but differ in purpose, flexibility, and performance:
Collectors.summingInt():
- Specialized collector designed specifically for summing integers
- More readable and concise for simple sums
- Better performance - optimized for numeric summation
- Returns primitive
int(orlong,double)
int totalSalary = employees.stream() .collect(Collectors.summingInt(Employee::getSalary));reducing():
- General-purpose reduction operation
- More flexible - works for any binary operation (sum, multiply, concatenate, etc.)
- Slightly less efficient for simple sums
- Returns
Optional<T>or T depending on overload
int totalSalary = employees.stream() .collect(Collectors.reducing( 0, // Identity value Employee::getSalary, // Mapper Integer::sum // Binary operator ));Key differences:
| Aspect | summingInt() | reducing() |
|---|---|---|
| Purpose | Specific to numeric sums | General reduction operations |
| Readability | More readable for sums | More verbose |
| Performance | Optimized for summation | General-purpose |
| Flexibility | Only sums | Any binary operation |
| Use Case | Simple numeric totals | Complex reductions |
When to use each:
- Use
summingInt()for straightforward sums - clearer intent and better performance - Use
reducing()when you need flexibility for non-sum operations or complex custom reductions
Example where reducing() is better:
// Finding product (can't use summingInt)int product = numbers.stream() .collect(Collectors.reducing(1, (a, b) -> a * b));
// Concatenating with custom logicString result = words.stream() .collect(Collectors.reducing("", (s1, s2) -> s1 + "-" + s2));