EC by Example: Collectors2
--
Learn how to transition to Eclipse Collections types using Collectors2
with any Java Stream
.
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 to CopyOnWriteArraySet
.
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
, orpartition
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.
I am a Project Lead and Committer for the Eclipse Collections OSS project at the Eclipse Foundation. Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.