Creating a custom collector via Collector.of() – Functional style programming – extending API
195. Creating a custom collector via Collector.of()
Creating a custom collector is a topic that we covered in detail in Java Coding Problem, First Edition, Chapter 9, Problem 193. More precisely, in that problem, you saw how to write a custom collector by implementing the java.util.stream.Collector interface.In this problem, we continue this journey, and we will create several custom collectors. This time, we will rely on two Collector#of() methods having the following signatures:
static <T,R> Collector<T,R,R> of(
Supplier<R> supplier,
BiConsumer<R,T> accumulator,
BinaryOperator<R> combiner,
Collector.Characteristics… characteristics)
static <T,A,R> Collector<T,A,R> of(
Supplier<A> supplier,
BiConsumer<A,T> accumulator,
BinaryOperator<A> combiner,
Function<A,R> finisher,
Collector.Characteristics… characteristics)
In this context, T, A, and R represent the following (pick-up from Java Coding Problems, First Edition):
T represents the type of elements from the Stream (elements that will be collected)
A represents the type of object that was used during the collection process known as the accumulator, which is used to accumulate the stream elements in a mutable result container.
R represents the type of the object after the collection process (the final result)
Moreover, a Collector is characterized by four functions and an enumeration. Again, here we have a short pick-up from Java Coding Problems, First Edition:These functions are working together to accumulate entries into a mutable result container, and optionally perform a final transformation on the result. They are as follows:
Creating a new empty mutable result container (the supplier argument)
Incorporating a new data element into the mutable result container (the accumulator argument)
Combining two mutable result containers into one (the combiner argument)
Performing an optional final transformation on the mutable result container to obtain the final result (the finisher argument)
In addition, we have the Collector.Characteristics… enumeration which defines the collector behavior. Possible values are UNORDERED (no order), CONCURRENT (more threads accumulate elements), and IDENTITY_FINISH (the finisher is the identity function so no further transformation will take place).In this context, let’s try to fire up a few examples. But, first, let’s assume that we have the following model:
public interface Vehicle {}
public class Car implements Vehicle {
private final String brand;
private final String fuel;
private final int horsepower;
…
}
public class Submersible implements Vehicle {
private final String type;
private final double maxdepth;
…
}
And, some data:
Map<Integer, Car> cars = Map.of(
1, new Car(“Dacia”, “diesel”, 100),
…
10, new Car(“Lexus”, “diesel”, 300)
);
Next, let’s have some collectors in a helper class named MyCollectors.
Writing a custom collector that collects into a TreeSet
In a custom collector that collects into a TreeSet we have that the supplier is TreeSet::new, the accumulator is TreeSet#add(), the combiner relies on TreeSet#addAll(), and the finisher is the identity function:
public static <T>
Collector<T, TreeSet<T>, TreeSet<T>> toTreeSet() {
return Collector.of(TreeSet::new, TreeSet::add,
(left, right) -> {
left.addAll(right);
return left;
}, Collector.Characteristics.IDENTITY_FINISH);
}
In the following example, we use this collector to collect all electric brands in a TreeSet<String>:
TreeSet<String> electricBrands = cars.values().stream()
.filter(c -> “electric”.equals(c.getFuel()))
.map(c -> c.getBrand())
.collect(MyCollectors.toTreeSet());
That was easy!