Definition
- teeing(downstream1, downstream2, merger) – (Java 12+) delivers each element to two collectors and combines their results with a
BiFunction. - minBy(comparing(salary)) and maxBy(comparing(salary)) – the two downstreams used for the salary extremes.
- Downstream results – both return
Optionalbecause the stream might be empty. - The merger – unwraps the Optionals and builds a result object like
new SalaryRange(min, max). - Single pass – the stream is traversed once, not twice.
Priya at the National Bank of India wants both the lowest and highest salary from the staff list. Normally she would walk the list once for the minimum and again for the maximum. With teeing she walks once: as each salary card passes, she updates two tallies simultaneously, a 'lowest so far' on the left and a 'highest so far' on the right. At the end she pairs them up. Two results, one walk.
Understanding the Concepts
What does this question ask?
The interviewer wants you to compute two aggregates in a single stream traversal using Collectors.teeing. This tests knowledge of the teeing collector, Optional handling, and result composition.
How do you compute min and max salary with teeing?
package com.bank.hr;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class TeeingMinMax {
record SalaryRange(Employee lowest, Employee highest) {}
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
SalaryRange range = employees.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparing(Employee::getSalary)),
Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
(minOpt, maxOpt) -> new SalaryRange(
minOpt.orElseThrow(),
maxOpt.orElseThrow())));
System.out.println("Lowest: " + range.lowest().getName()
+ " Rs." + range.lowest().getSalary());
System.out.println("Highest: " + range.highest().getName()
+ " Rs." + range.highest().getSalary());
}
}
Lowest: Neha Rs.700000
Highest: Rajesh Rs.1800000
The teeing collector feeds every employee to both minBy and maxBy simultaneously. The merger receives two Optional values and combines them into a SalaryRange record.
How does teeing work internally?
Element 1 (Rajesh) ──→ minBy accumulator + maxBy accumulator
Element 2 (Priya) ──→ minBy accumulator + maxBy accumulator
...
Element N (Meera) ──→ minBy accumulator + maxBy accumulator
Stream ends → minBy finishes → Optional<Employee>
maxBy finishes → Optional<Employee>
merger(minOpt, maxOpt) → SalaryRange
What was the alternative before Java 12?
Before teeing, you had three options: two separate streams, DoubleSummaryStatistics, or a custom collector.
// Option 1: Two separate streams (two passes)
Optional<Employee> min = employees.stream()
.min(Comparator.comparing(Employee::getSalary));
Optional<Employee> max = employees.stream()
.max(Comparator.comparing(Employee::getSalary));
// Option 2: DoubleSummaryStatistics (one pass, but loses Employee reference)
DoubleSummaryStatistics stats = employees.stream()
.collect(Collectors.summarizingDouble(
e -> e.getSalary().doubleValue()));
System.out.println("Min: " + stats.getMin() + ", Max: " + stats.getMax());
// Only gives the salary amount, not the Employee object
// Option 3: Java 12+ teeing (one pass, keeps Employee reference)
SalaryRange range = employees.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparing(Employee::getSalary)),
Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
(lo, hi) -> new SalaryRange(lo.orElseThrow(), hi.orElseThrow())));
How do you use teeing for non-numeric aggregates?
Teeing is not limited to min/max. The two downstreams can be any collectors.
// Count + joined names in one pass
record CountAndNames(long count, String names) {}
CountAndNames result = employees.stream()
.collect(Collectors.teeing(
Collectors.counting(),
Collectors.mapping(Employee::getName,
Collectors.joining(", ")),
CountAndNames::new));
System.out.println(result.count() + " employees: " + result.names());
10 employees: Rajesh, Priya, Amit, Kavita, Suresh, Deepak, Neha, Vikram, Ananya, Meera
What are common mistakes?
Mistake 1: forgetting that minBy/maxBy return Optional.
// WRONG: merger receives Optional, not Employee
Employee wrong = employees.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparing(Employee::getSalary)),
Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
(min, max) -> min)); // min is Optional<Employee>, not Employee!
// CORRECT: unwrap the Optional
Employee correct = employees.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparing(Employee::getSalary)),
Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
(min, max) -> min.orElseThrow()));
Mistake 2: using teeing when summarizingDouble would suffice.
// Overkill: teeing just for numeric min and max
// Use summarizingDouble instead - simpler and works on Java 8+
DoubleSummaryStatistics stats = employees.stream()
.collect(Collectors.summarizingDouble(
e -> e.getSalary().doubleValue()));
What about handling empty streams with teeing?
// Safe handling of empty stream
Optional<SalaryRange> safeRange = employees.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparing(Employee::getSalary)),
Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
(minOpt, maxOpt) -> minOpt.flatMap(min ->
maxOpt.map(max -> new SalaryRange(min, max)))));
safeRange.ifPresentOrElse(
r -> System.out.println(r.lowest().getName() + " to " + r.highest().getName()),
() -> System.out.println("No employees"));
What are common interview follow-up questions?
- "Can you use teeing with groupingBy?" Yes, teeing can be a downstream of groupingBy for per-group dual aggregation.
- "What if you need three aggregates?" Nest teeing:
teeing(collector1, teeing(collector2, collector3, merger2), merger1). - "Is teeing parallel-safe?" Yes, as long as both downstreams are parallel-safe.
Visual Overview
Stream<Employee>
│
│ .collect(teeing(minBy, maxBy, merger))
▼
┌──────────────────────────────────────────────┐
│ Element ──→ minBy accumulator │
│ ──→ maxBy accumulator │
│ │
│ (each element feeds BOTH collectors) │
└──────────────────────────────────────────────┘
│
│ stream ends
▼
minBy result: Optional[Neha Rs.700K]
maxBy result: Optional[Rajesh Rs.1800K]
│
│ merger(minOpt, maxOpt)
▼
SalaryRange(lowest=Neha, highest=Rajesh)
Key Takeaway
Collectors.teeing, added in Java 12, computes two aggregates in a single pass by feeding every stream element to two downstream collectors simultaneously. For min and max salary, the first downstream is minBy and the second is maxBy, both returning Optional. The merger BiFunction unwraps the Optionals and combines them into a result object.
Before Java 12, you would either stream the data twice (once for min, once for max) or use DoubleSummaryStatistics, which gives numeric min and max but loses the Employee reference. Teeing preserves the full object because the downstreams work on Employee, not on double values.
Teeing is general-purpose: the two downstreams can be any collectors. You can combine counting with joining, or minBy with toList, or any other pair. For three aggregates, nest teeing inside teeing.
Remember one sentence: teeing feeds every element to two collectors at once and merges their results, giving you two aggregates in one pass instead of two.
---