EC by Example: Collectors2

Learn how to transition to Eclipse Collections types using Collectors2 with any Java Stream.

Visualizing Collectors2

Anatomy of a Collector

One of the many great additions to Java 8 was the interface named Collector. A Collector can be used with the collect method on the Stream interface. The collect method will allow you to reduce a Stream to any type you want. Java 8 included a set of stock Collector implementations which are part of the Collectors utility class. Eclipse Collections includes another set of Collector implementations that return Eclipse Collections types. The name of the utility class in Eclipse Collections is Collectors2.

So what is a Collector? Let’s take a look at the interface to find out. There are five public instance methods on a Collector.

  • supplier → Supplier<A>
  • accumulator → BiConsumer<A, T>
  • combiner → BinaryOperator<A>
  • finisher → Function<A, R>
  • characteristics → Set<Characteristics> → Enum(CONCURRENT, UNORDERED, IDENTITY_FINISH)

There are also two static of methods on Collector which can be used to easily create your own Collector implementations.

So let’s see how we can create a Collector to better understand what these individual components are used for.

@Test
public void collector()
{
Collector<String, Set<String>, Set<String>> toCOWASet =
Collector.of(
HashSet::new, // supplier
Set::add, // accumulator
(set1, set2) -> { // combiner
set1.addAll(set2);
return set1;
},
CopyOnWriteArraySet::new); // finisher
List<String> strings = Arrays.asList("a", "b", "c");
Set<String> set =
strings.stream().collect(toCOWASet);
Assert.assertEquals(new HashSet<>(strings), set);
}

Here I use the static of method which takes five parameters. I leave the var arg’d final parameter for characteristics empty here. The supplier here creates a new HashSet. The accumulator is used to specify what operation to apply on the object created using the supplier. The items in the Stream will be passed to the add method of the Set. The combiner is used to specify how collections should be merged in the case where a parallelStream is used. I cannot use a method reference for the combiner here because one of the collections must be returned, and the addAll method on Collection returns a boolean. Finally, the finisher coverts the final result to a CopyOnWriteArraySet.

Building a reusable Collector

The Collector example above would not be very useful if it needed to be inlined directly in code as it is rather verbose. It would be much more useful if it could handle any type instead of just String. This can be done easily by moving the construction of this Collector to a static method and giving it a name like toCopyOnWriteArraySet.

public static <T> Collector<T, ?, Set<T>> toCopyOnWriteArraySet()
{
return Collector.<T, Set<T>, Set<T>>of(
HashSet::new, // supplier
Set::add, // accumulator
(set1, set2) -> { // combiner
set1.addAll(set2);
return set1;
},
CopyOnWriteArraySet::new, // finisher
Collector.Characteristics.UNORDERED); // characteristics
}

@Test
public void reusableCollector()
{
List<String> strings = Arrays.asList("a", "b", "c");
Set<String> set1 =
strings.stream().collect(toCopyOnWriteArraySet());
Verify.assertInstanceOf(CopyOnWriteArraySet.class, set1);
Assert.assertEquals(new HashSet<>(strings), set1);

List<Integer> integers = Arrays.asList(1, 2, 3);
Set<Integer> set2 =
integers.stream().collect(toCopyOnWriteArraySet());
Verify.assertInstanceOf(CopyOnWriteArraySet.class, set2);
Assert.assertEquals(new HashSet<>(integers), set2);
}

Now I’ve created a reusable Collector that can be used with a Stream of any type. I’ve additionally specified a Collector.Characteristics in the reusable implementation. These characteristics can be used by the Stream collect method to optimize the reduction implementation. Since I am accumulating to a Set which is unordered in this case, it makes sense to use the UNORDERED characteristic.

Filtering with Collectors2

In order to filter with Collectors2, you will need three things:

  • A select, reject, or partition Collector
  • A Predicate
  • A target collection Supplier

Here are examples using select, reject, and partition with Collectors2.

@Test
public void filtering()
{
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Predicate<Integer> evens = i -> i % 2 == 0;

MutableList<Integer> selectedList = list.stream().collect(
Collectors2.select(evens, Lists.mutable::empty));
MutableSet<Integer> selectedSet = list.stream().collect(
Collectors2.select(evens, Sets.mutable::empty));

MutableList<Integer> rejectedList = list.stream().collect(
Collectors2.reject(evens, Lists.mutable::empty));
MutableSet<Integer> rejectedSet = list.stream().collect(
Collectors2.reject(evens, Sets.mutable::empty));

PartitionList<Integer> partitionList = list.stream().collect(
Collectors2.partition(evens, PartitionFastList::new));
PartitionSet<Integer> partitionSet = list.stream().collect(
Collectors2.partition(evens, PartitionUnifiedSet::new));

Assert.assertEquals(selectedList, partitionList.getSelected());
Assert.assertEquals(rejectedList, partitionList.getRejected());

Assert.assertEquals(selectedSet, partitionSet.getSelected());
Assert.assertEquals(rejectedSet, partitionSet.getRejected());
}

Transforming with Collectors2

There are several methods which provide different transformations using Collectors2. The most basic transformation is available through the collect method. In order to use collect, you will need two things:

  • A Function
  • A target collection Supplier

The other transforming Collectors I will demonstrate below are makeString, zip, zipWithIndex, chunk, and flatCollect.

@Test
public void transforming()
{
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
MutableList<String> strings = list.stream().collect(
Collectors2.collect(Object::toString,
Lists.mutable::empty));

String string = list.stream().collect(Collectors2.makeString());

Assert.assertEquals(string, strings.makeString());

MutableList<Pair<Integer, String>> zipped =
list.stream().collect(Collectors2.zip(strings));

Assert.assertEquals(Tuples.pair(1, "1"), zipped.getFirst());
Assert.assertEquals(Tuples.pair(5, "5"), zipped.getLast());

MutableList<ObjectIntPair<Integer>> zippedWithIndex =
list.stream().collect(Collectors2.zipWithIndex());

Assert.assertEquals(
PrimitiveTuples.pair(Integer.valueOf(1), 0),
zippedWithIndex.getFirst());
Assert.assertEquals(
PrimitiveTuples.pair(Integer.valueOf(5), 4),
zippedWithIndex.getLast());

MutableList<MutableList<Integer>> chunked =
list.stream().collect(Collectors2.chunk(2));

Assert.assertEquals(
Lists.mutable.with(1, 2), chunked.getFirst());
Assert.assertEquals(
Lists.mutable.with(5), chunked.getLast());

MutableList<Integer> flattened = chunked.stream().collect(
Collectors2.flatCollect(e -> e, Lists.mutable::empty));

Assert.assertEquals(list, flattened);
}

Converting with Collectors2

There are two sets of converting Collector implementations available in Collectors2. One set converts to MutableCollection types. The other converts to ImmutableCollection types.

Collectors converting to Mutable Collections

@Test
public void convertingToMutable()
{
List<Integer> source = Arrays.asList(2, 1, 4, 3, 5);
MutableBag<Integer> bag = source.stream().collect(
Collectors2.toBag());
MutableSortedBag<Integer> sortedBag = source.stream().collect(
Collectors2.toSortedBag());
Assert.assertEquals(
Bags.mutable.with(1, 2, 3, 4, 5), bag);
Assert.assertEquals(
SortedBags.mutable.with(1, 2, 3, 4, 5), sortedBag);

MutableSet<Integer> set = source.stream().collect(
Collectors2.toSet());
MutableSortedSet<Integer> sortedSet = source.stream().collect(
Collectors2.toSortedSet());
Assert.assertEquals(
Sets.mutable.with(1, 2, 3, 4, 5), set);
Assert.assertEquals(
SortedSets.mutable.with(1, 2, 3, 4, 5), sortedSet);

MutableList<Integer> list = source.stream().collect(
Collectors2.toList());
MutableList<Integer> sortedList = source.stream().collect(
Collectors2.toSortedList());
Assert.assertEquals(
Lists.mutable.with(2, 1, 4, 3, 5), list);
Assert.assertEquals(
Lists.mutable.with(1, 2, 3, 4, 5), sortedList);

MutableMap<String, Integer> map =
source.stream().limit(4).collect(
Collectors2.toMap(Object::toString, e -> e));
Assert.assertEquals(
Maps.mutable.with("2", 2, "1", 1, "4", 4, "3", 3),
map);

MutableBiMap<String, Integer> biMap =
source.stream().limit(4).collect(
Collectors2.toBiMap(Object::toString, e -> e));
Assert.assertEquals(
BiMaps.mutable.with("2", 2, "1", 1, "4", 4, "3", 3),
biMap);
}

Collectors converting to Immutable Collections

@Test
public void convertingToImmutable()
{
List<Integer> source = Arrays.asList(2, 1, 4, 3, 5);
ImmutableBag<Integer> bag = source.stream().collect(
Collectors2.toImmutableBag());
ImmutableSortedBag<Integer> sortedBag = source.stream().collect(
Collectors2.toImmutableSortedBag());
Assert.assertEquals(
Bags.immutable.with(1, 2, 3, 4, 5), bag);
Assert.assertEquals(
SortedBags.immutable.with(1, 2, 3, 4, 5), sortedBag);

ImmutableSet<Integer> set = source.stream().collect(
Collectors2.toImmutableSet());
ImmutableSortedSet<Integer> sortedSet = source.stream().collect(
Collectors2.toImmutableSortedSet());
Assert.assertEquals(
Sets.immutable.with(1, 2, 3, 4, 5), set);
Assert.assertEquals(
SortedSets.immutable.with(1, 2, 3, 4, 5), sortedSet);

ImmutableList<Integer> list = source.stream().collect(
Collectors2.toImmutableList());
ImmutableList<Integer> sortedList = source.stream().collect(
Collectors2.toImmutableSortedList());
Assert.assertEquals(
Lists.immutable.with(2, 1, 4, 3, 5), list);
Assert.assertEquals(
Lists.immutable.with(1, 2, 3, 4, 5), sortedList);

ImmutableMap<String, Integer> map =
source.stream().limit(4).collect(
Collectors2.toImmutableMap(
Object::toString, e -> e));
Assert.assertEquals(
Maps.immutable.with("2", 2, "1", 1, "4", 4, "3", 3),
map);

ImmutableBiMap<String, Integer> biMap =
source.stream().limit(4).collect(
Collectors2.toImmutableBiMap(
Object::toString, e -> e));
Assert.assertEquals(
BiMaps.immutable.with("2", 2, "1", 1, "4", 4, "3", 3),
biMap);
}

The Collector implementations that convert to ImmutableCollection types use the finisher to convert from a mutable container to an immutable container. Here is the example of the Collector implementation for toImmutableList().

public static <T> Collector<T, ?, ImmutableList<T>> toImmutableList()
{
return Collector.<T, MutableList<T>, ImmutableList<T>>of(
Lists.mutable::empty, // supplier
MutableList::add, // accumulator
MutableList::withAll, // combiner
MutableList::toImmutable, // finisher
EMPTY_CHARACTERISTICS); // characteristics
}

The finisher here is the MutableList::toImmutable method reference. This is the final step that converts the MutableCollection with the results into an ImmutableCollection.

Eclipse Collections API vs. Collectors2

My preference is always to use the Eclipse Collections API directly if I can. If I need to operate on a JDK Collection type or if I am only given a Stream, then I will use Collectors2. As you can see in the examples above, Collectors2 is a natural gateway to the Eclipse Collections types and their functional, fluent, friendly and fun APIs.

Check out this presentation to learn more about the origins, design and evolution of the Eclipse Collections API.

Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.