0Pricing
Java Academy · Lesson

Building a Custom Collector

Implement the Collector interface to create a reusable aggregation for a domain-specific use case.

Building a Custom Collector is a free Java Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Java Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Build a Custom Collector?

When built-in collectors don't fit your domain, implement the Collector<T, A, R> interface to create reusable, composable aggregation logic. T=input, A=mutable accumulator, R=result.

The Collector Interface

A Collector has four components:

  • supplier() — creates the initial accumulator
  • accumulator() — folds one element into the accumulator
  • combiner() — merges two accumulators (for parallel streams)
  • finisher() — transforms the accumulator into the final result
  • characteristics() — optimization hints

Collector.of() Factory

The easiest way to create a custom collector is Collector.of() without implementing the interface directly:

import java.util.stream.*;

// Collector that builds a comma-separated String
Collector<String, StringBuilder, String> csvCollector = Collector.of(
    StringBuilder::new,                     // supplier
    (sb, s) -> {
        if (sb.length() > 0) sb.append(',');
        sb.append(s);
    },                                       // accumulator
    (a, b) -> {
        if (a.length() > 0 && b.length() > 0) a.append(',');
        a.append(b); return a;
    },                                       // combiner
    StringBuilder::toString                  // finisher
);

String csv = Stream.of("Alice","Bob","Carol").collect(csvCollector);
System.out.println(csv); // Alice,Bob,Carol

Custom Collector: TopN

Build a collector that keeps only the top N elements by a comparator:

import java.util.*;
import java.util.stream.*;

static <T> Collector<T, ?, List<T>> topN(int n, Comparator<T> comp) {
    return Collector.of(
        () -> new ArrayList<T>(),
        (list, e) -> {
            list.add(e);
            list.sort(comp.reversed());
            if (list.size() > n) list.remove(list.size() - 1);
        },
        (a, b) -> {
            a.addAll(b);
            a.sort(comp.reversed());
            while (a.size() > n) a.remove(a.size() - 1);
            return a;
        },
        Collector.Characteristics.UNORDERED
    );
}

Using TopN Collector

Apply the custom TopN collector on a stream:

List<Integer> top3 = Stream.of(5,2,9,1,7,3,8)
    .collect(topN(3, Comparator.naturalOrder()));
System.out.println(top3); // [9, 8, 7]

Characteristics Flags

Three optional characteristics tune stream optimization:

  • IDENTITY_FINISH — finisher is identity; skip it
  • UNORDERED — result order doesn't matter
  • CONCURRENT — accumulator can be called concurrently

Setting the wrong flag (e.g., CONCURRENT without thread-safety) causes bugs.

Custom Collector: Running Average

Maintain a running average without a second pass:

record RunningAvg(long count, double sum) {
    RunningAvg add(double v) { return new RunningAvg(count+1, sum+v); }
    RunningAvg merge(RunningAvg o) { return new RunningAvg(count+o.count, sum+o.sum); }
    double avg() { return count == 0 ? 0 : sum / count; }
}

Collector<Double, RunningAvg[], Double> avgCollector = Collector.of(
    () -> new RunningAvg[]{new RunningAvg(0, 0)},
    (a, v) -> a[0] = a[0].add(v),
    (a, b) -> { a[0] = a[0].merge(b[0]); return a; },
    a -> a[0].avg()
);

double avg = Stream.of(10.0, 20.0, 30.0).collect(avgCollector);
System.out.println(avg); // 20.0

Implementing the Collector Interface Directly

For more control, implement Collector<T,A,R> as a class:

import java.util.function.*;
import java.util.stream.*;

class StringJoiningCollector implements Collector<String, List<String>, String> {
    private final String delimiter;
    StringJoiningCollector(String delimiter) { this.delimiter = delimiter; }
    public Supplier<List<String>> supplier() { return ArrayList::new; }
    public BiConsumer<List<String>, String> accumulator() { return List::add; }
    public BinaryOperator<List<String>> combiner() {
        return (a,b) -> { a.addAll(b); return a; };
    }
    public Function<List<String>, String> finisher() {
        return list -> String.join(delimiter, list);
    }
    public Set<Characteristics> characteristics() { return Set.of(); }
}

Comparing Custom vs Built-in

Before writing a custom collector, check if composing built-in collectors achieves the same result — they're already tested and optimized:

// Instead of custom, compose:
Map<String, IntSummaryStatistics> stats = orders.stream().collect(
    Collectors.groupingBy(Order::status,
        Collectors.summarizingInt(Order::amount))
);
// This replaces many custom collector implementations

Thread Safety in Custom Collectors

If CONCURRENT characteristic is set, the accumulator is called from multiple threads simultaneously. The accumulator must then be thread-safe (e.g., use ConcurrentHashMap or AtomicLong). Without CONCURRENT, the combiner handles parallelism safely.

Real Use Case: Histogram Collector

Bucket numeric values into a histogram map:

Collector<Integer, Map<Integer, Long>, Map<Integer, Long>> histogram =
    Collector.of(
        LinkedHashMap::new,
        (map, val) -> map.merge(val / 10 * 10, 1L, Long::sum),
        (a, b) -> { b.forEach((k,v) -> a.merge(k,v,Long::sum)); return a; },
        Collector.Characteristics.IDENTITY_FINISH
    );

Map<Integer,Long> hist = Stream.of(5,15,25,12,8,22,18,3)
    .collect(histogram);
hist.forEach((bucket, cnt) -> System.out.println(bucket+"-"+(bucket+9)+": "+cnt));

Quick Check

Which Collector.Characteristics flag indicates that the finisher function is the identity transformation and can be skipped?

Recap: Custom Collectors

Key takeaways:

  • Collector<T,A,R>: T=input, A=accumulator, R=result
  • Use Collector.of() for quick implementation without a class
  • supplier/accumulator/combiner/finisher/characteristics are the 5 parts
  • IDENTITY_FINISH, UNORDERED, CONCURRENT are optimization flags
  • Prefer composing built-in collectors; go custom only when needed

Frequently asked questions

Is the “Building a Custom Collector” lesson free?

Yes — the full text of “Building a Custom Collector” is free to read here on the web, and the Java Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Java Academy course, upgrade to CoddyKit PRO.

What will I learn in “Building a Custom Collector”?

Implement the Collector interface to create a reusable aggregation for a domain-specific use case. You practise Java Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Java Academy?

No prior experience is required. Java Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Building a Custom Collector” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Java Academy lesson?

Yes. Every Java Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. groupingBy: Classifying Elements
  2. partitioningBy and counting
  3. toMap, joining, and summarizing
  4. Building a Custom Collector
← Back to Java Academy