By yourself some time
The preposition By
, and its positive contribution to productivity.
Work smart by leveraging intention revealing Java APIs
We all have limited time. Anything that saves us time coding, and improves the readability of our code is wonderful. Readability is key, as we read code much more than we write it. Many people may have to read a piece of code over its lifetime, so saving them time is a real bonus.
What can you By with Eclipse Collections to save time?
You can aggregate
, group
, sum
, count
, min
, max
and convert to other sorted collections by applying some Function
. The following By
methods are available on every RichIterable
subtype in Eclipse Collections.
I will show three examples from the Eclipse Collections Pet Kata that illustrate how these methods can save you time reading and writing code.
CountBy
Use countBy
if you want to count a collection of things by an attribute or result of applying a Function
to those things. The method takes the type Function
as a parameter and returns a Bag
. This method was added in Eclipse Collections 9.0.
The following code is an imperative Java solution for taking a List
of PetType
and counting each PetType
, by using a Map
.
List<PetType> petTypes =
this.people.flatCollect(Person::getPets).collect(Pet::getType);
Map<PetType, Integer> counts = Maps.mutable.empty();
for (PetType petType : petTypes)
{
Integer count = petTypeCounts.get(petType);
if (count == null)
{
count = 0;
}
petTypeCounts.put(petType, count + 1);
}
Assert.assertEquals(Integer.valueOf(2), counts.get(PetType.CAT));
Assert.assertEquals(Integer.valueOf(2), counts.get(PetType.DOG));
// The following test would fail as the Map would return null
Assert.assertEquals(Integer.valueOf(0), counts.get(PetType.CONDOR));
This code can be simplified by using Java 8 Streams and the groupingBy
and counting
Collectors.
Map<PetType, Long> counts =
this.people.stream()
.flatMap(person -> person.getPets().stream())
.collect(Collectors.groupingBy(Pet::getType,
Collectors.counting()));
Assert.assertEquals(Long.valueOf(2), counts.get(PetType.CAT));
Assert.assertEquals(Long.valueOf(2), counts.get(PetType.DOG));
// The following test would fail as the Map would still return null
Assert.assertEquals(Long.valueOf(0), counts.get(PetType.CONDOR));
The following solution will use Eclipse Collections Bag
type instead of a Map
. A Bag
is a Map
of keys to counts. The HashBag
implementation in Eclipse Collections uses an ObjectIntHashMap
for its storage, so does not require boxing the counts as Integer
or Long
.
Bag<PetType> counts =
this.people.asLazy()
.flatCollect(Person::getPets)
.collect(Pet::getType)
.toBag();
Assert.assertEquals(2, counts.occurrencesOf(PetType.CAT));
Assert.assertEquals(2, counts.occurrencesOf(PetType.DOG));
Assert.assertEquals(0, counts.occurrencesOf(PetType.CONDOR));
This code takes less time to read because there are fewer characters, but the method toBag
will only communicate its intent to you if you already know what a Bag
is.
Here is a slight modification to this solution using countBy
.
Bag<PetType> counts =
this.people.asLazy()
.flatCollect(Person::getPets)
.countBy(Pet::getType);
Assert.assertEquals(2, counts.occurrencesOf(PetType.CAT));
Assert.assertEquals(2, counts.occurrencesOf(PetType.DOG));
Assert.assertEquals(0, counts.occurrencesOf(PetType.CONDOR));
CountBy
reduces the code and communicates the intent better. We are counting all of the pets by their PetType
. The result is still a Bag
, as that is the best data structure to return.
GroupBy
Use groupBy
if you want to group elements of a collection using a function to calculate a key for each element. The method takes the type Function
as a parameter and returns a Multimap
.
The following code is an imperative Java solution for taking a List
of Person
and grouping them by their last names.
Map<String, List<Person>> lastNamesToPeople = Maps.mutable.empty();
for (Person person : this.people)
{
String lastName = person.getLastName();
List<Person> peopleWithLastName =
lastNamesToPeople.get(lastName);
if (peopleWithLastName == null)
{
peopleWithLastName = Lists.mutable.empty();
lastNamesToPeople.put(lastName, peopleWithLastName);
}
peopleWithLastName.add(person);
}
Assert.assertEquals(3, lastNamesToPeople.get("Smith").size());
This code can be simplified using Java 8 Streams and the groupingBy
method on Collectors
.
Map<String, List<Person>> lastNamesToPeople =
this.people.stream()
.collect(Collectors.groupingBy(Person::getLastName));
Assert.assertEquals(3, lastNamesToPeople.get("Smith").size());
// This code will throw a NullPointerException
Assert.assertEquals(0, lastNamesToPeople.get("Smith1").size());
This code can be further simplified by using groupBy
from Eclipse Collections. Unlike groupingBy
on Collectors
which returns a Map
and will return null for missing keys, groupBy
returns a Multimap
. A Multimap
will return empty collections for missing keys.
Multimap<String, Person> lastNamesToPeople =
this.people.groupBy(Person::getLastName);
Assert.assertEquals(3, lastNamesToPeople.get("Smith").size());
// This assertion passes, as a Multimap will return an empty list
Assert.assertEquals(0, lastNamesToPeople.get("Smith1").size());
GroupByEach
Use groupByEach
if you want to group elements of a collection together using a function that calculates multiple keys for each element. The method takes the type Function
as a parameter and returns a Multimap
. The Function
must return a subtype of Iterable
.
Here’s an imperative Java example taking the list of Person and grouping them by the PetType
s they own, making sure the PetType
s are unique.
Map<PetType, Set<Person>> peopleByPetType = Maps.mutable.empty();
for (Person person : this.people)
{
MutableList<Pet> pets = person.getPets();
for (Pet pet : pets)
{
PetType petType = pet.getType();
Set<Person> peopleWithPetType =
peopleByPetType.get(petType);
if (peopleWithPetType == null)
{
peopleWithPetType = Sets.mutable.empty();
peopleByPetType.put(petType, peopleWithPetType);
}
peopleWithPetType.add(person);
}
}
Assert.assertEquals(2, peopleByPetType.get(PetType.CAT).size());
Assert.assertEquals(2, peopleByPetType.get(PetType.DOG).size());
I am not aware of a way to do this using Java 8 Streams with Collectors
today, without building my own Collector
. Here is the solution using Eclipse Collections groupByEach
method.
Multimap<PetType, Person> multimap =
this.people.groupByEach(
Person::getPetTypes,
Multimaps.mutable.set.empty());
Assert.assertEquals(2, multimap.get(PetType.CAT).size());
Assert.assertEquals(2, multimap.get(PetType.DOG).size());
In the above example, I needed to pass an empty MutableSetMultimap
as a target collection to populate, because by default a MutableListMultimap
would be have been returned. This is because this.people is a MutableList
. If the field this.people
was a SetIterable
or subclass, the following would work.
Multimap<PetType, Person> multimap =
this.people.groupByEach(Person::getPetTypes);
Assert.assertEquals(2, multimap.get(PetType.CAT).size());
Assert.assertEquals(2, multimap.get(PetType.DOG).size());
Don’t let opportunities for time savings pass you By
High-level methods like countBy
, groupBy
and groupByEach
can make quantitative and qualitative improvements to your code. You can save yourself and your team a lot of time over the life of your project by using higher level APIs such as the By
methods in Eclipse Collections.
By the by, sometimes its the little things in life and APIs that make a big difference.
Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.