Immutable List Collector in Java 8

If you’ve watched any presentation on Java 8 and lambdas by an Oracle employee in the last year or so you probably haven’t missed the “list of shapes” example, it goes something like this:

This works perfectly fine but what if you want the boxes list to be immutable? You could of course do like this:

However this is a bit awkward to use (especially if you want to do this in a lot of places throughout your code base). What we want is something more fluid:

ImmutableListCollector is not shipped with Java 8 so we’ll have to create it ourselves. The collect method takes an instance of java.util.stream.Collector but we don’t have to create a concrete implementation of this class. Instead we can make use of the static of method defined in the Collector interface. This is essentially a builder that allows us to create our own Collector instance in a little more concise way. There are several overloaded variants of the of method but we’re going to use the one that looks like this:

Here’s a short explaination of the parameters (for a full explaination see the Javadoc for java.util.stream.Collector) and what we’re using them for:

  • The supplier is a functional interface (which means that it only defines one method) whose get method will return the resulting object that will be populated by the collector. In our case we’ll create a supplier that returns new ArrayList. An instance of this supplier (an ArrayList instance) will be returned (as immutable) by the collector when it’s finished.
  • The accumulator is also a functional interface whose accept method is used to add an element from the stream into the ArrayList created by the supplier. Once all elements have been added to the ArrayList we’re ready to convert it into an immutable list.
  • The combiner is yet another functional interface that takes two operands of the same type, producing a result of the same type as the operands. In our case it’s used by the Collector to combine two ArrayList instances into one. This function is called by the collector when the stream is in parallel mode.
  • Characteristics let’s you specify properties that provide hints to the Collector how it can optimize reduction implementations. In our case we’ll leave this blank.

You may wonder if we won’t run into ConcurrentModificationException‘s when the collector is used in for parallel streams? Not to worry! When the stream is in parallel mode (e.g. shapes.stream().parallel()) the supplier function is called multiple times and no more than one thread will append elements to each supplier instance (ArrayList in our case) at the same time. The combiner function is used to merge the results of two ArrayList‘s produced by the accumulator function. For more information on how to algorithm works refer to the javadocs of the java.util.stream.Collector class.

Implementation

As you can see the implementation uses the Collector.of method and the first argument is the supplier function. In Java 8 we can use <class>::new to create a supplier that’ll create a new instance of this particular class when called by the collector. In our case we create an array list and thus use ArrayList::new.

The second parameter is the accumulator function used to add elements from the stream into our ArrayList. To do this we can use the add method of the list. Since the collector doesn’t care about the return value of the add(..) method (the BiConsumer‘s accept method is void) we can simply reference the add method using List::add.

Next up is the combiner function where we need to combine the results of two different supplier instances (created when the stream is in parallel mode). So if we have two ArrayList‘s then we can combine them by calling the addAll method. The BinaryOperator‘s apply method doesn’t return void but rather (in our case) an instance of ArrayList and thus we cannot simply use a method reference to List::addAll since addAll returns void. For this reason we create a slightly longer lambda function that adds the elements of the right array list into the left one. The left list is then returned as the result of the combiner function.

The fourth argument to the Collector.of method is the finisher function. By now the collector have merged all elements in the stream into an ArrayList provided by our supplier. We use this finisher function to convert our generated ArrayList into an immutable counterpart by referencing the unmodifiableList method.

There’s one slight variation we could do to improve our ImmutableListCollector:

The consumer of the collector can now determine what kind of immutable list that it wants. For example if we want to return an immutable LinkedList instead of an ArrayList in the shapes example we could do like this:

Conclusion

Writing custom collectors can take a while to get used to but on the upside you get to experiment with some of the new interesting features of Java 8. Once you have a collector up and running it’s easy to use and re-use in (possibly) different use cases. In the future we’ll probably see a lot of libraries built on top of Java’s new streaming API that uses various collector implementations to solve problems in a functional way.

This Post Has 6 Comments

  1. Ugg. Tried for a few hours to figure out how to write a custom Collector for a Google Guava LinkedHashMultimap. I attempted to implement the Collector interface but get hung up on how the generics for the type. Have any of you successfully implemented such a beast? And I thought Scala types were hard ;-)

  2. Hi, actually you don’t need to implement your own ImmutableCollector. For this purpose you can use collectingAndThen.

    List immutableBoxes = shapes.stream()
    .filter(s -> s.getColor() == Color.BLUE)
    .map(s -> s.getContainingBox())
    .collect(collectingAndThen(toList(), Collections::unmodifiableList));

    1. Thanks for the tip, didn’t know of this construct.

  3. If you’re using Google Guava, you can also do:

    .collect(Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf))

  4. Awesome explanation. Very helpful.

    Thank you.

Leave a Reply

Close Menu