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 accumulatoraccumulator()— folds one element into the accumulatorcombiner()— merges two accumulators (for parallel streams)finisher()— transforms the accumulator into the final resultcharacteristics()— 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,CarolCustom 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 itUNORDERED— result order doesn't matterCONCURRENT— 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.0Implementing 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 implementationsThread 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
- groupingBy: Classifying Elements
- partitioningBy and counting
- toMap, joining, and summarizing
- Building a Custom Collector