We learn best by doing
In this blog, I will show you how to get started coding the Eclipse Collections Pet kata. If you want to become proficient in Eclipse Collections, you should practice the kata on your own. In order to truly master the kata, try teaching the kata to others. Teaching results in deeper learning.
Follow the leader
There are slides online that I will walk through, as well as links to the code in GitHub. I will complete the first exercise of the Pet Kata. There are 4 failing tests in which I will demonstrate 19 different solutions. Follow along in your Java IDE. Try the different solutions that I share below.
Step 1 – Download Eclipse Collections kata
- Clone the repo here— https://github.com/eclipse/eclipse-collections-kata
- Import the pom.xml file located in the root as a Maven Project in your Java IDE
- Select the Pet Kata folder and go to the tests directory
- Start with Exercise 1
- Run the unit tests — They should fail!
- Get the tests to pass one by one
- When you complete the last exercise— Congratulations!
- Star our repo if you enjoy the kata
The Slides
The online slides for the tutorial portion of the Pet Kata can be found here.
Understanding the Pet KATA domain
Person, Pet and PetType are the domain classes in the kata. A Person can have zero or more pets. Each Pet has exactly one PetType. PetType is an enum. See the domain model for more details.
Exercise 1 – Lessons
The collect pattern is used to transform one collection type to another. This pattern is also known as map or transform.
The select pattern is used to filter a collection on a positive condition. This pattern is also known as filter.
Exercise 1 – Fix The FAILING tests
Test #1 – Get First Names of People
@Test public void getFirstNamesOfAllPeople() { // Replace null, with a transformation method on MutableList. MutableList<String> firstNames = null; // this.people... MutableList<String> expectedFirstNames = Lists.mutable.with("Mary", "Bob", "Ted", "Jake", "Barry", "Terry", "Harry", "John"); Assert.assertEquals(expectedFirstNames, firstNames); }
In the first test, we need to write the code to collect the names of the people contained in “this.people”. The names returned should match the value of expectedFirstNames. The collect method takes a Function as a parameter. I will replace the null value for the firstNames variable with different solutions.
Solution (1) – Use a Lambda with collect
MutableList<String> firstNames = this.people.collect(person -> person.getFirstName());
Solution (2) – Use a Method Reference with collect
MutableList<String> firstNames = this.people.collect(Person::getFirstName);
I prefer using method references whenever possible. Method references are more succinct, and are often self-documenting.
Solution (3) – Use a Java Stream with Collectors2
MutableList<String> firstNames = this.people.stream() .map(Person::getFirstName) .collect(Collectors2.toList());
Eclipse Collections has a static utility class named Collectors2. There are Collector implementations on this class that return Eclipse Collections types.
Solution (4) – Use an Anonymous Inner Class with collect
MutableList<String> firstNames = this.people.collect(new Function<Person, String>() { @Override public String valueOf(Person person) { return person.getFirstName(); } });
I would not usually write code like this in Java 8 or above. However, it is useful sometimes if you want to see all of the types that are involved. Most noteworthy, this code will still work in any version of Java after Java 5.
Test #2 – Get Names of Mary Smith’s Pets
@Test public void getNamesOfMarySmithsPets() { Person person = this.getPersonNamed("Mary Smith"); MutableList<Pet> pets = person.getPets(); // Replace null, with a transformation method on MutableList. MutableList<String> names = null; // pets... Assert.assertEquals("Tabby", names.makeString()); }
In the second test, we want to collect the names of “Mary Smith’s” pets. Mary Smith has one pet named “Tabby”. Repetition is good for learning. We will use the collect method again to obtain the names of “Mary Smith’s” pets.
Solution (5) – Use a Lambda with collect
MutableList<String> names = pets.collect(pet -> pet.getName());
Solution (6) – Use a Method Reference with collect
MutableList<String> names = pets.collect(Pet::getName);
Solution (7) – Use a Java Stream with Collectors2
MutableList<String> names = pets.stream() .map(Pet::getName) .collect(Collectors2.toList());
Solution (8) – Use an Anonymous Inner Class with collect
MutableList<String> names = pets.collect(new Function<Pet, String>() { @Override public String valueOf(Pet pet) { return pet.getName(); } });
Test #3 – Get People with Cats
@Test public void getPeopleWithCats() { // Replace null, with a positive filtering method on MutableList. MutableList<Person> peopleWithCats = null; // this.people... Verify.assertSize(2, peopleWithCats); }
In the third test, we want to find all of the people who have cats. As a result, we will use the select method. The select method takes a Predicate as a parameter.
Solution (9) – Use a Lambda with select
MutableList<Person> peopleWithCats = this.people.select(person -> person.hasPet(PetType.CAT));
Solution (10) – Use a Method Reference with selectWith
MutableList<Person> peopleWithCats = this.people.selectWith(Person::hasPet, PetType.CAT);
We cannot use a method reference with the select method in this example. We can use the method selectWith, which takes a Predicate2 and a parameter.
Solution (11) – Use a Java Stream with Collectors2
MutableList<Person> peopleWithCats = this.people.stream() .filter(person -> person.hasPet(PetType.CAT)) .collect(Collectors2.toList());
We cannot use a method reference with the filter method in this case.
Solution (12) – Use a Java Stream with Collectors2.selectWith()
MutableList<Person> peopleWithCats = this.people.stream() .collect(Collectors2.selectWith( Person::hasPet, PetType.CAT, Lists.mutable::empty));
We can use a method reference with the selectWith method on Collectors2. The selectWith method takes three parameters – a Predicate2, a parameter to pass to the Predicate2 and finally, a Supplier.
Solution (13) – Use reduceInPlace with Collectors.selectWith()
MutableList<Person> peopleWithCats = this.people.reduceInPlace(Collectors2.selectWith( Person::hasPet, PetType.CAT, Lists.mutable::empty));
We can reduce the boilerplate of stream().collect() by just calling reduceInPlace().
Solution (14) – Use an Anonymous Inner Class with select
MutableList<Person> peopleWithCats = this.people.select(new Predicate<Person>() { @Override public boolean accept(Person person) { return person.hasPet(PetType.CAT); } });
Solution (15) – Use an Anonymous Inner Class with selectWith
MutableList<Person> peopleWithCats = this.people.selectWith(new Predicate2<Person, PetType>() { @Override public boolean accept(Person person, PetType petType) { return person.hasPet(petType); } }, PetType.CAT);
Test #4 – Get People without Cats
@Test public void getPeopleWithoutCats() { // Replace null, with a negative filtering method on MutableList. MutableList<Person> peopleWithoutCats = null; // this.people... Verify.assertSize(6, peopleWithoutCats); }
In final test, we want to find all of the people who do not have cats. There is a natural opposite to select available in Eclipse Collections. The method named reject, filters based on the negative result of applying a Predicate.
Solution (16) – Use a Lambda with reject
MutableList<Person> peopleWithoutCats = this.people.reject(person -> person.hasPet(PetType.CAT));
Solution (17) – Use a Method Reference with rejectWith
MutableList<Person> peopleWithoutCats = this.people.rejectWith(Person::hasPet, PetType.CAT);
Eclipse Collections has good symmetry in its API. As a result, since there is a selectWith method, there is a rejectWith counterpart.
Solution (18) – Use a Java Stream with Collectors2
MutableList<Person> peopleWithoutCats = this.people.stream() .collect(Collectors2.rejectWith( Person::hasPet, PetType.CAT, Lists.mutable::empty));
There is no natural opposite of filter in Java Streams (filter IN vs. filter OUT). However, we can use rejectWith method available on Collectors2 with a method reference.
Solution (19) – Use an Anonymous Inner Class with reject
MutableList<Person> peopleWithoutCats = this.people.reject(new Predicate<Person>() { @Override public boolean accept(Person person) { return person.hasPet(PetType.CAT); } });
For further study
Try solving the Eclipse Collections Pet kata on your own. Use method references and lambdas. Use Java Streams with and without Collectors2. Experiment with primitive collections in the later exercises. See how different JVM language like Groovy, Kotlin or Scala can solve the kata. There is an amazing amount of potential for learning just on the first exercise alone.
Footnote
Eclipse Collections is open for contributions. You can also contribute to the kata. If you like the library, you can let us know by starring it on GitHub.
Author: donraab
Java Champion. Creator of the Eclipse Collections Java library (http://www.eclipse.org/collections/). Inspired by Smalltalk. Opinions are my own.