Back to Collectors
Q10Question 10 of 21

Collectors.teeing: two collectors in one pass, merge results (Java 12+)

Key Concepts
  • Collectors.teeing runs two collectors simultaneously on the same stream in a single pass
  • Each element is fed to BOTH downstream collectors independently
  • The merger BiFunction combines both results into one final value when the stream is exhausted
  • More efficient than two separate streams because data is iterated only once
  • Named after a plumbing T-joint: one input splits into two outputs, then merged
  • Java 12+ (JEP not numbered, added as API enhancement)
  • Works with any two collectors: minBy+maxBy, counting+summing, filtering+grouping

Definition

Collectors.teeing runs two collectors on the same stream in one pass and merges their results with a BiFunction, computing two aggregates simultaneously.
  • Signature – teeing(Collector downstream1, Collector downstream2, BiFunction merger).
  • Behavior – every stream element is fed to BOTH downstream collectors. When the stream ends, the merger combines both results.
  • Efficiency – one iteration instead of two; important for large datasets or I/O-backed streams.
  • Naming – named after a plumbing T-joint: one input pipe splits into two outputs, then the outputs are joined.

Rajesh needs both the smallest and largest transaction from his bank statement. Without teeing, the bank scans all 1000 transactions once for the smallest, then scans again for the largest. With teeing, one scanner reads each transaction and simultaneously updates two trackers (min and max). At the end, both results are available. Half the work.

Understanding the Concepts

What is Collectors.teeing?

Collectors.teeing (Java 12) runs two collectors on the same stream simultaneously and merges their results.

How do you compute min and max in one pass?

JAVA
package com.bank.analytics;

import java.math.BigDecimal;
import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Collectors;

record SalaryRange(Optional<BigDecimal> min, Optional<BigDecimal> max) {}

public class SalaryAnalytics {

    public SalaryRange getRange(java.util.List<BigDecimal> salaries) {
        return salaries.stream()
            .collect(Collectors.teeing(
                Collectors.minBy(Comparator.naturalOrder()),  // downstream 1
                Collectors.maxBy(Comparator.naturalOrder()),  // downstream 2
                SalaryRange::new));                            // merger
    }
}
TEXT
Input: [900000, 1200000, 1500000, 1800000]
Result: SalaryRange{min=Optional[900000], max=Optional[1800000]}
One pass through the data.

Why is one pass better than two?

JAVA
// WRONG: two passes over the same data
Optional<BigDecimal> min = salaries.stream().min(Comparator.naturalOrder());
Optional<BigDecimal> max = salaries.stream().max(Comparator.naturalOrder());
// Two iterations!

// CORRECT: one pass with teeing
var range = salaries.stream().collect(Collectors.teeing(
    Collectors.minBy(Comparator.naturalOrder()),
    Collectors.maxBy(Comparator.naturalOrder()),
    SalaryRange::new));
// One iteration!

How do you compute count and sum together?

JAVA
record CountAndSum(long count, BigDecimal sum) {
    BigDecimal average() {
        return count == 0 ? BigDecimal.ZERO : sum.divide(
            BigDecimal.valueOf(count), 2, java.math.RoundingMode.HALF_UP);
    }
}

CountAndSum result = trades.stream()
    .map(Trade::getAmount)
    .collect(Collectors.teeing(
        Collectors.counting(),
        Collectors.reducing(BigDecimal.ZERO, BigDecimal::add),
        CountAndSum::new));
// CountAndSum{count=6, sum=1185737}
// result.average() → 197622.83

How do you use teeing with filtering?

JAVA
// Separate settled and unsettled totals in one pass
record SettlementSummary(BigDecimal settledTotal, BigDecimal unsettledTotal) {}

SettlementSummary summary = trades.stream()
    .collect(Collectors.teeing(
        Collectors.filtering(Trade::isSettled,
            Collectors.mapping(Trade::getAmount,
                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))),
        Collectors.filtering(t -> !t.isSettled(),
            Collectors.mapping(Trade::getAmount,
                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))),
        SettlementSummary::new));

Can you nest teeing for more than two results?

JAVA
// Four metrics in one pass: min, max, count, sum
record FullStats(SalaryRange range, CountAndSum countSum) {}

FullStats stats = amounts.stream()
    .collect(Collectors.teeing(
        Collectors.teeing(                                    // inner teeing 1
            Collectors.minBy(Comparator.naturalOrder()),
            Collectors.maxBy(Comparator.naturalOrder()),
            SalaryRange::new),
        Collectors.teeing(                                    // inner teeing 2
            Collectors.counting(),
            Collectors.reducing(BigDecimal.ZERO, BigDecimal::add),
            CountAndSum::new),
        FullStats::new));                                      // outer merger
// All four metrics computed in a single pass!

What are common mistakes?

JAVA
// WRONG: merger returns wrong type
var result = stream.collect(Collectors.teeing(
    Collectors.counting(),
    Collectors.summingInt(Trade::getQuantity),
    (count, sum) -> count));  // discards sum! Why use teeing then?

// CORRECT: use both results
var result = stream.collect(Collectors.teeing(
    Collectors.counting(),
    Collectors.summingInt(Trade::getQuantity),
    (count, sum) -> new CountAndQuantity(count, sum)));

What should a senior engineer know?

  • For empty streams, both downstream collectors return their identity values (Optional.empty for minBy/maxBy, 0 for counting).
  • teeing is especially powerful as a downstream inside groupingBy: per-group min+max in one pass.
  • The merger BiFunction can be a record constructor (cleanest), a lambda, or any BiFunction.

What are common interview follow-up questions?

  • Can you have more than two collectors? (Not directly. Nest teeing calls or use a custom collector.)
  • Does teeing work with parallel streams? (Yes. Both downstream collectors handle parallel splitting independently.)
  • What if one downstream returns Optional.empty? (The merger receives Optional.empty; handle it there.)

Visual Overview

DIAGRAM
  Collectors.teeing(minBy, maxBy, SalaryRange::new)

  Stream: 900K → 1200K → 1500K → 1800K
           │       │        │       │
           ▼       ▼        ▼       ▼
    ┌────────────────────────────────────┐
    │ Downstream 1 (minBy):              │
    │ 900K → 900K → 900K → 900K = 900K  │
    ├────────────────────────────────────┤
    │ Downstream 2 (maxBy):              │
    │ 900K → 1200K → 1500K → 1800K      │
    └────────────────┬───────────────────┘
                     │ merger
                     ▼
    SalaryRange(min=900K, max=1800K)
    One pass through the data.

Key Takeaway

Collectors.teeing runs two collectors on the same stream in a single pass and merges their results with a BiFunction. Each element is fed to both downstream collectors simultaneously. The merger combines both results into one final value when the stream is exhausted.

This is more efficient than creating separate streams: for min+max, teeing makes one pass instead of two. For count+sum, one pass instead of two. For nested teeing with four metrics (min, max, count, sum), one pass instead of four.

In a banking analytics service, computing the salary range (min and max) with teeing(minBy, maxBy, SalaryRange::new) produces both extremes in a single iteration. The merger can be a record constructor for the cleanest code. For empty streams, both downstream collectors return their identity values and the merger handles them.

Remember one sentence: Collectors.teeing runs two collectors in one pass and merges their results, computing multiple aggregates simultaneously instead of iterating twice.

---

Share
Use ← → arrow keys to navigate