Definition
- **Supplier**: creates a new mutable result container, like `() -> new StringBuilder()` or `() -> new BigDecimal[]{BigDecimal.ZERO}`.
- **Accumulator**: adds one element to the container, like `(sb, s) -> sb.append(s)` or `(acc, t) -> acc[0] = acc[0].add(t.amount())`.
- **Combiner**: merges two partial containers for parallel streams, like `(sb1, sb2) -> sb1.append(sb2)`. Must be associative.
- **Finisher**: transforms the container into the final result type, like `sb -> sb.toString()`. Optional with `IDENTITY_FINISH`.
- **Characteristics**: hints like `UNORDERED`, `CONCURRENT`, `IDENTITY_FINISH` for optimization.
- Write a custom collector only when built-in collectors cannot express your accumulation pattern.Built-in collectors are like pre-made bank forms. A custom collector is designing your own form with four parts: the blank form (supplier), instructions for filling one field (accumulator), instructions for merging two half-filled forms (combiner), and instructions for finalizing the completed form (finisher).
Understanding the Concepts
When do you need a custom collector?
You need a custom collector when the built-in collectors (toList, groupingBy, joining, reducing) cannot express your accumulation pattern. Common cases: collecting to a custom data structure, accumulating BigDecimal with exact precision, or building a formatted output.
How do you build a custom BigDecimal sum collector?
package com.bank.util;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collector;
public class CustomCollectorDemo {
record Transaction(String id, BigDecimal amount) {}
public static void main(String[] args) {
List<Transaction> txns = List.of(
new Transaction("T001", new BigDecimal("50000.50")),
new Transaction("T002", new BigDecimal("75000.25")),
new Transaction("T003", new BigDecimal("30000.75"))
);
// Custom BigDecimal sum collector
Collector<Transaction, BigDecimal[], BigDecimal> bigDecimalSum =
Collector.of(
() -> new BigDecimal[]{BigDecimal.ZERO}, // supplier
(acc, t) -> acc[0] = acc[0].add(t.amount()), // accumulator
(a, b) -> {
a[0] = a[0].add(b[0]);
return a;
}, // combiner
a -> a[0] // finisher
);
BigDecimal total = txns.stream().collect(bigDecimalSum);
System.out.println("Total: Rs." + total);
}
}
Total: Rs.155001.50
How do you build a custom formatted statement collector?
Collector<Transaction, StringBuilder, String> statementCollector =
Collector.of(
StringBuilder::new, // supplier
(sb, t) -> sb.append(t.id())
.append(": Rs.")
.append(t.amount())
.append("\n"), // accumulator
(sb1, sb2) -> sb1.append(sb2), // combiner
StringBuilder::toString // finisher
);
String statement = txns.stream().collect(statementCollector);
System.out.println(statement);
T001: Rs.50000.50
T002: Rs.75000.25
T003: Rs.30000.75
What are the five components?
| Component | Purpose | Example | |-----------|---------|----------| | Supplier | Create empty container | `() -> new StringBuilder()` | | Accumulator | Add one element | `(sb, s) -> sb.append(s)` | | Combiner | Merge two containers (parallel) | `(sb1, sb2) -> sb1.append(sb2)` | | Finisher | Transform to final result | `sb -> sb.toString()` | | Characteristics | Optimization hints | `IDENTITY_FINISH`, `UNORDERED` |
What are common mistakes?
WRONG: A combiner that is not associative.
// WRONG: subtraction is not associative
// (a - b) - c != a - (b - c)
Collector.of(
() -> new int[]{0},
(acc, n) -> acc[0] -= n,
(a, b) -> { a[0] -= b[0]; return a; } // WRONG for parallel!
);
WRONG: Forgetting the combiner. The combiner is required even if you never use parallel streams.
What should a senior engineer know?
- Prefer composing existing collectors before writing custom ones.
reducing,mapping,collectingAndThen, andteeing(Java 12) cover most cases. - A custom collector with
CONCURRENTcharacteristic can use a concurrent container (e.g.,ConcurrentLinkedQueue) and skip the combiner phase in parallel. - The
IDENTITY_FINISHcharacteristic tells the framework to skip the finisher call (the container IS the result). - Custom collectors are testable: write unit tests that verify the supplier, accumulator, combiner, and finisher independently.
Visual Overview
Sequential stream:
Supplier: new BigDecimal[]{ZERO}
│
Accumulator: add T001.amount → [50000.50]
Accumulator: add T002.amount → [125000.75]
Accumulator: add T003.amount → [155001.50]
│
Finisher: a[0] → BigDecimal(155001.50)
Parallel stream:
Thread 1 Thread 2
Supplier: [ZERO] Supplier: [ZERO]
+ T001 → [50000.50] + T002 → [75000.25]
+ T003 → [105001.00]
│ │
└──────┬───────────────┘
▼
Combiner: [50000.50] + [105001.00] → [155001.50]
│
▼
Finisher: BigDecimal(155001.50)
Key Takeaway
A custom collector is built with Collector.of specifying five components: supplier creates the mutable container, accumulator adds one element, combiner merges two containers for parallel streams, finisher transforms the container into the final result, and characteristics provide optimization hints.
Write a custom collector only when built-in collectors cannot express your pattern. For most cases, composing reducing, mapping, collectingAndThen, and teeing is sufficient.
At National Bank of India, a custom BigDecimal sum collector accumulates transaction amounts with exact decimal precision, avoiding the rounding errors of summingDouble. The combiner ensures correct parallel execution.
Remember one sentence: supplier creates the container, accumulator fills it, combiner merges parallel parts, and finisher transforms the result.
---