By yourself some time

Donald Raab
Javarevisited
Published in
4 min readDec 3, 2017

The preposition By, and its positive contribution to productivity.

De Palm Island, Aruba — A place I would like to spend some more time

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.

By methods in RichIterable (ignore the collectByte methods)

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 PetTypes they own, making sure the PetTypes 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.

--

--

Javarevisited
Javarevisited

Published in Javarevisited

A humble place to learn Java and Programming better.

Donald Raab
Donald Raab

Written by Donald Raab

Java Champion. Creator of the Eclipse Collections OSS Java library (https://github.com/eclipse/eclipse-collections). Inspired by Smalltalk. Opinions are my own.