Yet another Java 8 custom collector example

Java 8 introduces a number of functional programming techniques to the language. Collections can be turned into streams which allows you to perform standard functional operations on them, such as filtering, mapping, reducing and collecting. In this post I’m going to give a quick example of writing a custom collector. Firstly, what is collecting? I would probably define it as “taking a collection / stream and forming it into a particular collection / data structure”. Java 8 has numerous helper methods and classes for standard collection operations, and you should use them when they apply. e.g. you can use a groupingBy collector to process a stream and group the elements by a property, producing a map, keyed off of that property, in which each value is a list of elements with that property. However, for more complex collecting, you will need to write your own collector. There are five methods in a collector:

  • supplier – returns a function, that takes no arguments, and returns an empty instance of the collection class you want to put your collected elements into. e.g. if you are ultimately collecting your elements into a set, the supplier function will return an empty set.
  • accumulator – returns a function that takes two arguments, the first is the collection that you are building up, the second is the element being processed. The accumulator function processes each element into the target collection.
  • finisher – returns a function that allows you to perform a final transformation on your collection, if required. In many cases, you won’t need to transform the collection any further, so you will just use an identity function here.
  • combiner – only required for parallel processing of your stream. If you envisage running this operation across multiple processors / cores, then your combiner contains the logic to combine results from each parallel operation.
  • characteristics – allows you to specify the characteristics of the collector so that that it can be invoked safely and optimally. e.g. specifying Characteristics.IDENTITY_FINISH lets Java know that because you aren’t performing a final transformation, it doesn’t even need to invoke your finisher function.

Okay, let’s do a trivial example. I work in insurance, so I’ll create an example in this domain. Suppose I have a stream of insurance claims. The claims may be of different types, such as motor, household etc. I want to produce a map with one example claim in, for each of a list of specified claim types. This needs a supplier function that gives me an empty map to start with, and an accumulator function that simply gets the claim type, and if the map doesn’t already contain an example claim of this type, adds it in. The finisher can be the identity function. This is what it looks like:

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
 
public class ClaimProductTypeCollector<T extends Claim> implements Collector<T,Map,Map> {
 
    private Set<Claim.PRODUCT_TYPE> requiredTypes = new HashSet<>();
 
    public Set<Claim.PRODUCT_TYPE> getRequiredTypes() {
        return requiredTypes;
    }
 
    @Override
    public Supplier<Map> supplier() {
        return () -> new HashMap<>();
    }
 
    @Override
    public BiConsumer<Map,T> accumulator() {
        return (map,claim) -> {
            if (map.get(claim.getProductType()) == null) {
                map.put(claim.getProductType(),claim);
            }
        };
    }
 
 
    @Override
    public Function<Map, Map> finisher() {
        return Function.identity();
    }
 
    @Override
    public BinaryOperator<Map> combiner() {
        return null;
    }
 
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.singleton(Characteristics.IDENTITY_FINISH);
    }
}

If you want to type this in and get it working as an example, here is what the claim class looks like:

public class Claim {
 
    public enum PRODUCT_TYPE { MOTOR, HOUSEHOLD, TRAVEL}
 
    private PRODUCT_TYPE productType;
 
    public Claim(PRODUCT_TYPE productType) {
        this.productType = productType;
    }
 
    public PRODUCT_TYPE getProductType() {
        return productType;
    }
 
    public void setProductType(PRODUCT_TYPE productType) {
        this.productType = productType;
    }
 
}

Then you can test it with:

Set<Claim> claims = new HashSet<>();
claims.add(new Claim(Claim.PRODUCT_TYPE.MOTOR));
claims.add(new Claim(Claim.PRODUCT_TYPE.MOTOR));
claims.add(new Claim(Claim.PRODUCT_TYPE.MOTOR));
 
claims.add(new Claim(Claim.PRODUCT_TYPE.HOUSEHOLD);
claims.add(new Claim(Claim.PRODUCT_TYPE.HOUSEHOLD);
 
ClaimProductTypeCollector<Claim> claimProductTypeCollector = new ClaimProductTypeCollector();
claimProductTypeCollector.getRequiredTypes().add(Claim.PRODUCT_TYPE.MOTOR);
claimProductTypeCollector.getRequiredTypes().add(Claim.PRODUCT_TYPE.HOUSEHOLD);
Map oneClaimPerProductType = claims.stream().collect(claimProductTypeCollector);

For more info on Java 8, I strongly recommend the book “Java 8 In Action”. You can get the eBook directly from the publishers Manning:

https://www.manning.com/books/java-8-in-action

This entry was posted in Java and tagged . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

501,820 Spambots Blocked by Simple Comments

HTML tags are not allowed.