Creating a custom collector in Java Streams allows you to define how elements from a stream are accumulated into a result, providing more flexibility than the built-in collectors like toList() or toSet(). To create a custom collector, you implement the Collector<T, A, R> interface, which has four key methods:

Steps to Create a Custom Collector

  1. supplier():
    • Provides an initial value (e.g., a container like a List, Set, or Map) for accumulating the stream elements.
  2. accumulator():
    • Defines how to add each element of the stream to the accumulator.
  3. combiner():
    • Defines how to combine two accumulators (used in parallel streams).
  4. finisher():
    • Converts the accumulated value into the final result.
  5. characteristics():
    • Returns a set of Collector.Characteristics that describe the behavior of the collector (e.g., CONCURRENT, UNORDERED).

Example: Custom Collector that Concatenates Strings with a Separator

Here’s how you can create a custom collector that joins strings with a separator:

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

public class CustomCollector {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Java", "Python", "C++");

        // Creating a custom collector
        Collector<String, StringBuilder, String> joiner = Collector.of(
            StringBuilder::new,  // supplier: Create an empty StringBuilder
            StringBuilder::append,  // accumulator: Concatenate strings
            StringBuilder::append,  // combiner: Combine two StringBuilders
            StringBuilder::toString,  // finisher: Convert to String
            Collector.Characteristics.IDENTITY_FINISH  // characteristics
        );

        // Using the custom collector
        String result = words.stream()
                             .collect(joiner);
        System.out.println(result);  // Output: JavaPythonC++
    }
}

Explanation

  1. supplier(): We create a new StringBuilder to hold the concatenated result.
  2. accumulator(): Each string in the stream is appended to the StringBuilder.
  3. combiner(): If the stream is processed in parallel, we combine two StringBuilder objects by appending them together.
  4. finisher(): Once the stream has been processed, the StringBuilder is converted to a String.
  5. characteristics(): We specify IDENTITY_FINISH, which indicates that the finisher() method returns the same type as the accumulator.

When to Use Custom Collectors

  • Complex Accumulations: When the built-in collectors do not meet your needs (e.g., grouping elements with custom logic or performing complex transformations).
  • Performance Optimization: For cases where you need to handle parallel processing efficiently.
  • Custom Data Structures: When you need to accumulate results into a custom data structure.

In summary, creating a custom collector involves implementing the Collector interface’s methods to define how to accumulate, combine, and finalize the elements of a stream, offering flexibility for advanced stream processing.