Back to Stream Practice: Employee Analysis
Q11Question 11 of 43

Use Collectors.teeing to compute the min and max salary in a single pass

Key Concepts
  • Collectors.teeing(downstream1, downstream2, merger) feeds every element to two collectors simultaneously in one pass
  • The merger BiFunction combines the two results into a single return value
  • teeing(minBy(...), maxBy(...), merger) computes min and max salary without streaming twice
  • Both downstreams return Optional because the stream might be empty
  • Before Java 12, use DoubleSummaryStatistics or two separate streams for min and max
  • teeing is general-purpose: the two downstreams can be any collectors, not just numeric

Definition

Collectors.teeing feeds every stream element to two downstream collectors simultaneously and merges their results into one value, computing both min and max salary in a single pass.
  • 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 Optional because 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?

JAVA
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());
    }
}
TEXT
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?

DIAGRAM
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.

JAVA
// 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.

JAVA
// 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());
TEXT
10 employees: Rajesh, Priya, Amit, Kavita, Suresh, Deepak, Neha, Vikram, Ananya, Meera

What are common mistakes?

Mistake 1: forgetting that minBy/maxBy return Optional.

JAVA
// 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.

JAVA
// 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?

JAVA
// 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate