Back to Collectors
Q4Question 4 of 21

Custom Collector with Collector.of(): supplier, accumulator, combiner, finisher, characteristics

Key Concepts
  • Collector.of(supplier, accumulator, combiner, finisher, characteristics) creates a custom collector
  • Supplier creates the mutable result container (new StringBuilder, new ArrayList, BigDecimal array)
  • Accumulator adds one element to the container
  • Combiner merges two partial containers for parallel streams (must be associative)
  • Finisher transforms the container into the final result (optional with IDENTITY_FINISH)
  • Prefer composing existing collectors (reducing, mapping, collectingAndThen) before writing custom ones

Definition

Collector.of defines a custom collector with five components: supplier creates the container, accumulator adds elements, combiner merges for parallel, finisher transforms the result, and characteristics provide optimization hints.
DIAGRAM
- **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?

JAVA
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);
    }
}
TEXT
Total: Rs.155001.50

How do you build a custom formatted statement collector?

JAVA
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);
TEXT
T001: Rs.50000.50
T002: Rs.75000.25
T003: Rs.30000.75

What are the five components?

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

JAVA
// 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, and teeing (Java 12) cover most cases.
  • A custom collector with CONCURRENT characteristic can use a concurrent container (e.g., ConcurrentLinkedQueue) and skip the combiner phase in parallel.
  • The IDENTITY_FINISH characteristic 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

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

---

Share
Use ← → arrow keys to navigate