Better Programming

Advice for programmers.

Follow publication

What If Java Collections Had Eager Methods for Filter, Map, FlatMap?

Donald Raab
Better Programming
Published in
18 min readNov 6, 2023

--

Photo by Peter F on Unsplash

Three years ago, I worked with Nikhil Nanivadekar on an experimental collections framework in an open source kata repository, in preparation for a presentation we gave to the JCP Executive Committee in April 2020. I decided to write this blog to share the knowledge we learned with the broader Java community who may be interested in what we explored.

Read this blog if you want to learn how we were able to breathe new life into a twenty-five year old Java Collections framework using a few advanced Java language features. We all have to deal with legacy code bases and libraries. If you learn how to leverage the Java features discussed in this blog, you will be able to adopt strategies to keep your own legacy code bases both stable and able to evolve over long periods of time.

The Java language features I will demonstrate are as follows:

  1. Covariant Return Types (available since Java 5)
  2. Default and Static Methods on Interfaces (available since Java 8)
  3. Sealed Classes (final since Java 16)

Covariant Return Types is an important Java feature that I feel has gotten less coverage than the others listed above. The addition of Sequenced Collections in Java 21 brings some recent attention to this important feature that was added in Java 5. The SequencedCollection interface has a method named reversed that has covariant overrides with more specific return types in SequencedSet, Deque, List, etc.

Default Methods and Static Methods on interfaces are the real hot sauce for the recipe we used in the experiment. Default Methods on Interfaces worked very well together in this experiment with Covariant Return Types. I will discuss these three features throughout the rest of this blog.

I wrote a blog about Sealed Classes previously. I will link to the separate blog with more detail about how I implemented Immutable collection types using Sealed Types.

A high-protein Java Collection experiment

We explored an approach of adding eager iteration patterns directly to Java Collection interfaces. We used method names that matched the names of the lazy iteration patterns from the Java Stream interface and Collector utilities. The result was a set of new Read-only, Mutable, and Immutable Collection interfaces which extend the standard Java Collection interfaces.

The following UML class diagram shows the new interfaces, in cyan and purple, that were designed and coded as part of the experiment. The purple interfaces were part of a separate but related experiment that leverages Sealed Classes to provide structural, contractual, and verifiable immutability.

Note: I will refer to the diagram below occasionally when explaining some of the code examples throughout the rest of this blog.

An experiment adding new interfaces extending the Java Collections Framework

Read-Only Interfaces

The Read-only List, Set, and Bag interfaces are in cyan. These interfaces allow for common behaviors to exist between Mutable and Immutable versions of List, Set, and Bag. There is no relationship between java.util.List and the Read-only List, or java.util.Set and the Read-only Set. This allows these new interfaces to be contractually read-only. By contractually read-only, I mean there are no mutating methods on the interfaces like add or remove.

Mutable Interfaces

The Mutable interfaces all extend the corresponding interfaces in the java.util package. Since there is no Bag type in java.util, MutableBag only extends java.util.Collection through its inheritance relationship with MutableCollection.

Immutable Interfaces

The Immutable interfaces only extend java.lang.Iterable and the read-only interfaces. The relationship to java.lang.Iterable is through the read-only RichIterable interface.

In the rest of the blog, we’ll explore of some of the advanced Java language features we used to add high-protein eager iteration patterns to Java Collections. We’ll start by revisiting the coding overhead required to use Java Stream to solve a simple filtering problem. We will filter even numbers from a List, Set, and Bag.

High-carb iteration patterns with Java Stream

In order to use Java Stream with a java.util.Collection, you first have to call a “bun” method on a Collection with the name stream or parallelStream. The “protein” methods like filter, map, flatMap, can then be called on the Stream interface. Lazy methods like filter and map require an additional “bun” operation like forEach or collect with a Collector to be called in order for the code to execute.

Lazy filter example with Stream on MutableList

The following code examples show how to filter evens from a List of Integer using a Java Stream with a MutableList. MutableList is a new interface we created.

@Test
public void filterEvensFromListToList()
{
// Static of() method on MutableList interface
MutableList<Integer> list = MutableList.of(1, 2, 3);

// Lazy filter on Stream - 2 Carbs, 1 Protein
List<Integer> lazy = list.stream()
.filter(each -> each % 2 == 0)
.toList();

// Static of() method on ImmutableList interface
ImmutableList<Integer> expected = ImmutableList.of(2);
Assertions.assertEquals(expected, lazy);
}

There are static methods named of on MutableList and ImmutableList that create instances of these interfaces.

Using Java Stream, we have to call stream, filter, and toList. There are two bun methods (stream and toList), and one protein method (filter) in the middle.

Lazy filter example with Stream on MutableSet

Let’s see what the code looks like if we make the source collection a MutableSet.

@Test
public void filterEvensFromSetToSet()
{
// Static of() method on MutableSet interface
MutableSet<Integer> set = MutableSet.of(1, 2, 3);

// Lazy filter on Stream - 2 Carbs, 1 Protein, 1 Utility Method
Set<Integer> lazy = set.stream()
.filter(each -> each % 2 == 0)
.collect(Collectors.toSet());

// Static of() method on ImmutableSet interface
ImmutableSet<Integer> expected = ImmutableSet.of(2);
Assertions.assertEquals(expected, lazy);
}

There are static methods named of on MutableSet and ImmutableSet that create instances of these interfaces.

Using Java Stream, we have to call stream, filter, collect, and toSet. There are two bun methods (stream and collect), one protein method (filter) in the middle and an extra utility method (Collectors.toSet). The extra utility method is necessary because there is no toSet method available directly on Stream.

Lazy filter example with Stream on MutableBag

Let’s look at the code necessary to filter a Collection type the JDK does not include — MutableBag.

@Test
public void filterEvensFromBagToBag()
{
// Static of() method on MutableBag interface
MutableBag<Integer> bag = MutableBag.of(1, 2, 3);

// Lazy filter on Stream - 2 Carbs, 1 Protein, 1 Utility, 1 Method Ref
Bag<Integer> lazy = bag.stream()
.filter(each -> each % 2 == 0)
.collect(Collectors.toCollection(MutableBag::empty));

// Static of() method on ImmutableBag interface
ImmutableBag<Integer> expected = ImmutableBag.of(2);
Assertions.assertEquals(expected, lazy);
}

There are static methods named of on MutableBag and ImmutableBag that create instances of these interfaces.

Using Java Stream, we have to call stream, filter, collect, toCollection, and create a method reference for empty. There are two bun methods (stream and collect), one protein method (filter) in the middle, and an extra utility method (Collectors.toCollection) with a method reference (MutableBag::empty). The extra utility method and method reference are necessary because there is no Bag type in the JDK, and no toBag method on Stream.

Now let’s begin the tour of the high-protein iteration patterns we added on Collection interfaces.

High-protein iteration patterns with Collections

The difference between lazy methods on the Stream interface, and eager methods directly on Collection interfaces as depicted in the UML class diagram above is subtle and significant. With eager methods available directly on Collection interfaces, the usage and implementation code is simplified. I wrote the following blog in April 2020 to explain why eager is easier to learn than lazy.

I usually prefer using eager algorithms by default, and lazy algorithms when I see an opportunity for an optimization. Both eager and lazy algorithms are useful so I always want both available in my toolkit.

With eager iteration patterns, the extra “bun” methods (e.g. stream , collect, toList) are no longer necessary, and you can call the high-protein methods like filter and map directly on the Collection itself. The return type of these methods on each Collection type are Collection types themselves, and each subtype can provide covariant overrides that return the most specific type that is appropriate for the given subtype. In the case of a single protein method being used, performance may be improved as well using an eager iteration pattern over a lazy iteration pattern.

Eager filter example with MutableList

The following code examples show how to filter evens from a List of Integer using a MutableList eager implementation of filter.

@Test
public void filterEvensFromMutableListToMutableList()
{
// Static of() method on MutableList interface
MutableList<Integer> list = MutableList.of(1, 2, 3);

// Eager filter with covariant return on MutableList - 1 Protein
MutableList<Integer> eager =
list.filter(each -> each % 2 == 0);

// Static of() method on ImmutableList interface
ImmutableList<Integer> expected = ImmutableList.of(2);

Assertions.assertEquals(expected, eager);
}

The Covariant Return Type of the eager filter method for a MutableList is a MutableList.

Eager filter with MutableSet and MutableBag

The solutions using an eager filter method for MutableSet and MutableBag will look very similar to the one for MutableList above. The Covariant Return Type of the filter method for a MutableSet is a MutableSet. The Covariant Return Type of the filter method for a MutableBag is a MutableBag.

Now we’ll explore some of the neat language features that made these examples possible.

Static Methods on Interfaces

The first thing we will look at is the implementation of the Static Methods on MutableList and ImmutableList that I used to create instances of these two interfaces in the filter tests above.

MutableList Static Methods

There are several static methods added to the MutableList interface. I only use the of method in the filter example above.

Static methods on the MutableList interface

These methods allow a developer to create an empty MutableList, construct a MutableList with one element, or construct a MutableList from a vararg array using an overloaded of method.

Notice there is new class named ArrayList2 created in the static of methods of MutableList. The implementation of ArrayList2 was trivial. ArrayList2 simply extends ArrayList, and implements the MutableList interface, and overrides some constructors.

The following is the full class definition for ArrayList2.

ArrayList2 extends java.util.ArrayList and implements MutableList

ArrayList2 defines no state, and no behavior beyond what is available in ArrayList. The existing java.util.ArrayList class could implement this interface, and that should be the only change needed on ArrayList. No changes would have to be made to the current java.util.List interface.

MutableSet and MutableBag Static Methods

Similar to MutableList, the MutableSet and MutableBag interfaces have static methods named empty and of. For MutableSet, the implementation returned will be a HashSet2. HashSet2 extends java.util.HashSet, and implements the MutablSet interface. It only overrides the constructors from java.util.HashSet.

A Bag type does not exist in the JDK, so the HashBag class referenced in the MutableBag empty and of methods had to be implemented from scratch. HashBag contains the basic implementation details for a MutableBag implementation. HashBag leverages a new class called HashMap2 which extends java.util.HashMap. You can explore this code on your own and will see a MutableMap interface which is implemented by HashMap2.

Now we’ll explore the immense possibilities that Default Methods on Interfaces creates.

Default Methods on Interfaces

Since Java 8, we have been able to define default methods on interfaces. A default method allows you to define the implementation of a method on an interface. Classes that implement the interface will get the default implementation of the method, unless they provide a suitable override. This theoretically makes it “safe” to add new behaviors to interfaces in a library without breaking existing implementations. We know there are some caveats to this safety, especially if a library has existed for decades and been used extensively. Generally, unless there are collisions with methods defined in the wild, the default method feature works very well.

I wanted to see how far we could get defining new behaviors for Java Collection types by only defining default methods on extension interfaces like MutableList. We were able to get quite far. As you can see in ArrayList2 and HashSet2, we didn’t have to add any new behaviors on the classes . With the exception of constructor overrides, all we had to do was extend the interfaces.

In the rest of this section, I am discuss the eager implementations of filter, map, and flatMap. The rest of the behaviors we added can be seen in the class diagram above and investigated by browsing the code.

RichIterable Interface

At the top of the hierarchy is an interface named RichIterable. Abstract methods exist for filter, map, flatMap are defined on RichIterable. There are also default overloaded implementations of each of the methods which take a target collection as a parameter.

RichIterable interface

RichIterable filter

Here is the code for the abstract and default filter methods defined on RichIterable.

abstract filter and default filter with target as defined on RichIterable

The filter with target default method can take any Collection implementation as an argument, and that type becomes the return type of the method. This method is used in each of the subtypes to reduce code duplication.

RichIterable map

Here is the code for the abstract and default map methods defined on RichIterable.

abstract map and default map with target as defined on RichIterable

The map with target default method can take any Collection implementation as an argument, and that specific subtype is defined to be the return type of the method.

RichIterable flatMap

Here is the code for the abstract and default flatMap methods defined on RichIterable.

abstract flatMap and default flatMap with a target as defined on RichIterable

The flatMap with target default method can take any MutableCollection implementation as an argument, and that specific subtype is defined to be the return type of the method.

MutableCollection is used here as the parent return type instead of Collection because flatMap takes a Function that returns an Iterable. The Collection type only has an addAll method that takes a Collection. The MutableCollection interface defines a default addAllIterable method which can take any Iterable as a parameter.

Here is the code for addAllIterable defined on the MutableCollection interface, which extends RichIterable.

The method addAllIterable as defined on MutableCollection

You might notice the Pattern Matching for instanceof feature is used in this method.

MutableList Interface

We’ll start with the MutableList interface to explain how the three default methods with Covariant Return Types are defined. The default implementations of map, filter, flatMap provided on RichIterable help make the implementations on the subtypes trivial. The overrides are mostly providing the Covariant Return Types by calling the parent method passing in a specific type.

MutableList interface

MutableList filter

Here is the code for the default filter method defined on MutableList.

The default method filter as defined on MutableList

This method overrides the abstract filter method defined in RichIterable which returns RichIterable. The filter method on MutableList has a Covariant Return Type of MutableList. This means that if you call filter on a MutableList you get a MutableList back as a result.

MutableList map

Here is the code for the default map method defined on MutableList.

The default method map as defined on MutableList

This method overrides the abstract map method defined in RichIterable which returns RichIterable. The map method on MutableList has a Covariant Return Type of MutableList. This means that if you call map on a MutableList you get a MutableList back as a result.

MutableList flatMap

Here is the code for the default flatMap method defined on MutableList.

The default method flatMap as defined on MutableList

This method overrides the abstract flatMap method defined in RichIterable which returns RichIterable. The flatMap method on MutableList has a Covariant Return Type of MutableList. This means that if you call flatMap on a MutableList you get a MutableList back as a result.

MutableSet and MutableBag Interfaces

The default implementations of filter, map, and flatMap on the MutableSet and MutableBag interfaces will look very similar to the ones defined on MutableList. They build upon the same filter, map, and flatMap methods defined in RichIterable and return more specific types.

MutableSet and MutableBag interfaces

Default and Static Methods in Interfaces, combined with Covariant Return Types allowed us to provide significant extensions to the existing Java Collections Framework implementations.

Sealed Classes for Immutable Collection Types

I wrote about using Sealed Classes to implement ImmutableCollection types in Java providing structural, contractual, and verifiable immutability. The following blog explains the approach in detail. I hope you will enjoy reading this separate, but related experiment in collections design.

Testing the code with the Pet Kata

I decided to open up my IntelliJ IDE and take a look at the experimental collections framework again, and see how much of the Eclipse Collections Pet Kata I could solve using the types and implementations that Nikhil Nanivadekar and I had worked on back in early 2020. In order to implement the Pet kata, I needed to add some missing functionality. I spent a couple hours adding the necessary methods with tests. I added notEmpty, containsBy, countByEach, and groupByEach in a Pull Request to the Code Katas repo. I also refactored the the original filter, map, and flatMap implementations. I extracted overloads of these methods that take target collections up to RichIterable. This removed a lot of duplicate for loop code.

The types I used from the experimental framework in the Pet Kata were ImmutableList, Bag, MutableBag, MutableList, MutableSet, MutableListMultimap.

The eager methods I used in the Pet Kata were containsBy, countBy, map, MutableBag.empty, MutableList.of, ImmutableList.of, MutableBag.of, ImmutableList.empty, map, filter, filterNot, anyMatch, allMatch, count, findFirst, flatMap, countByEach, groupBy, groupByEach.

The following are my code solutions to exercises one through three of the Eclipse Collections Pet Kata. I am using Java 21 records for the Person and Pet classes. You can compare my solutions below, with the checked in solutions in the Pet Kata which use Eclipse Collections.

Person record

public record Person(String firstName, String lastName, ImmutableList<Pet> pets)
{
public String getFullName()
{
return this.firstName + ' ' + this.lastName;
}

public boolean named(String name)
{
return name.equals(this.getFullName());
}

public boolean hasPet(PetType petType)
{
return this.pets.containsBy(Pet::type, petType);
}

public boolean hasPet(String petEmoji)
{
return this.hasPet(PetType.fromEmoji(petEmoji));
}

public Bag<PetType> getPetTypes()
{
return this.pets.countBy(Pet::type);
}

public Bag<String> getPetEmojis()
{
return this.getPetTypes().map(Object::toString, MutableBag.empty());
}

public IntStream getPetAges()
{
return this.pets.stream().mapToInt(Pet::age);
}

public boolean isPetPerson()
{
return this.pets.notEmpty();
}
}

Pet record

public record Pet(PetType type, String name, int age)
{
@Override
public String toString()
{
return this.type.toString();
}
}

PetType Enum

public enum PetType
{
CAT("🐱"),
DOG("🐶"),
HAMSTER("🐹"),
TURTLE("🐢"),
BIRD("🐦"),
SNAKE("🐍");

private final String emoji;

PetType(String emoji)
{
this.emoji = emoji;
}

@Override
public String toString()
{
return this.emoji;
}

public static PetType fromEmoji(String searchEmoji)
{
return Stream.of(PetType.values())
.filter(petType -> petType.emoji.equals(searchEmoji))
.findFirst()
.orElse(null);
}
}

PetDomainForKata

public abstract class PetDomainForKata
{
protected MutableList<Person> people;

@BeforeEach
public void setUp() throws Exception
{
this.people = MutableList.of(
new Person("Mary", "Smith",
ImmutableList.of(new Pet(PetType.CAT, "Tabby", 2))),
new Person("Bob", "Smith" ,
ImmutableList.of(
new Pet(PetType.CAT, "Dolly", 3),
new Pet(PetType.DOG, "Spot", 2))),
new Person("Ted", "Smith",
ImmutableList.of(new Pet(PetType.DOG, "Spike", 4))),
new Person("Jake", "Snake",
ImmutableList.of(new Pet(PetType.SNAKE, "Serpy", 1))),
new Person("Barry", "Bird",
ImmutableList.of(new Pet(PetType.BIRD, "Tweety", 2))),
new Person("Terry", "Turtle",
ImmutableList.of(new Pet(PetType.TURTLE, "Speedy", 1))),
new Person("Harry", "Hamster",
ImmutableList.of(
new Pet(PetType.HAMSTER, "Fuzzy", 1),
new Pet(PetType.HAMSTER, "Wuzzy", 1))),
new Person("John", "Doe", ImmutableList.empty())
);
}

public Person getPersonNamed(String fullName)
{
return this.people.findFirst(person -> person.named(fullName))
.orElse(null);
}
}

Exercise1Test

public class Exercise1Test extends PetDomainForKata
{
@Test
public void getFirstNamesOfAllPeople()
{
MutableList<String> firstNames =
this.people.map(Person::firstName);

var expectedFirstNames =
MutableList.of("Mary", "Bob", "Ted", "Jake", "Barry", "Terry", "Harry", "John");
Assertions.assertEquals(expectedFirstNames, firstNames);
}

@Test
public void getNamesOfMarySmithsPets()
{
Person person = this.getPersonNamed("Mary Smith");

ImmutableList<Pet> pets = person.pets();

ImmutableList<String> names =
pets.map(Pet::name);

Assertions.assertEquals(
"Tabby",
names.stream().collect(Collectors.joining("")));
}

@Test
@DisplayName("getPeopleWithCats 🐱")
public void getPeopleWithCats()
{
MutableList<Person> peopleWithCats =
this.people.filter(person -> person.hasPet(PetType.CAT));

var expectedLastNames = MutableList.of("Smith", "Smith");

Assertions.assertEquals(
expectedLastNames,
peopleWithCats.map(Person::lastName));
}

@Test
@DisplayName("getPeopleWithoutCats 🐱")
public void getPeopleWithoutCats()
{
MutableList<Person> peopleWithoutCats =
this.people.filterNot(person -> person.hasPet(PetType.CAT));

var expectedLastNames =
MutableList.of("Smith", "Snake", "Bird", "Turtle", "Hamster", "Doe");

Assertions.assertEquals(
expectedLastNames,
peopleWithoutCats.map(Person::lastName));
}
}

Exercise2Test

public class Exercise2Test extends PetDomainForKata
{
@Test
@DisplayName("doAnyPeopleHaveCats 🐱?")
public void doAnyPeopleHaveCats()
{
Predicate<Person> predicate =
person -> person.hasPet("🐱");

Assertions.assertTrue(this.people.anyMatch(predicate));
}

@Test
public void doAllPeopleHavePets()
{
boolean result =
this.people.allMatch(Person::isPetPerson);

Assertions.assertFalse(result);
}

@Test
@DisplayName("howManyPeopleHaveCats 🐱?")
public void howManyPeopleHaveCats()
{
int count =
this.people.count(person -> person.hasPet("🐱"));

Assertions.assertEquals(2, count);
}

@Test
public void findMarySmith()
{
Person result =
this.people.findFirst(person -> person.named("Mary Smith"))
.orElse(null);

Assertions.assertEquals("Mary", result.firstName());
Assertions.assertEquals("Smith", result.lastName());
}

@Test
@DisplayName("findPetNamedSerpy 🐍")
public void findPetNamedSerpy()
{
MutableList<Pet> petList =
this.people.flatMap(Person::pets);

Pet serpySnake =
petList.findFirst(pet -> pet.name().equals("Serpy"))
.orElse(null);

Assertions.assertEquals("🐍", serpySnake.type().toString());
}

@Test
public void getPeopleWithPets()
{
MutableList<Person> petPeople =
this.people.filter(Person::isPetPerson);

Assertions.assertEquals(7, petPeople.size());
}

@Test
public void getAllPetTypesOfAllPeople()
{
MutableSet<PetType> petTypes =
this.people.flatMap(Person::getPetTypes, MutableSet.empty());

var expected =
MutableSet.of(PetType.CAT,
PetType.DOG,
PetType.TURTLE,
PetType.HAMSTER,
PetType.BIRD,
PetType.SNAKE);
Assertions.assertEquals(expected, petTypes);
}

@Test
public void getAllPetEmojisOfAllPeople()
{
MutableSet<String> petEmojis =
this.people.flatMap(Person::getPetEmojis, MutableSet.empty());

var expected =
MutableSet.of("🐱", "🐶", "🐢", "🐹", "🐦", "🐍");
Assertions.assertEquals(expected, petEmojis);
}

@Test
public void getFirstNamesOfAllPeople()
{
MutableList<String> firstNames =
this.people.map(Person::firstName);

var expected =
MutableList.of("Mary", "Bob", "Ted", "Jake", "Barry", "Terry", "Harry", "John");
Assertions.assertEquals(expected, firstNames);
}

@Test
@DisplayName("doAnyPeopleHaveCatsRefactor 🐱?")
public void doAnyPeopleHaveCatsRefactor()
{
boolean peopleHaveCatsLazy =
this.people.stream().anyMatch(person -> person.hasPet("🐱"));
Assertions.assertTrue(peopleHaveCatsLazy);

boolean peopleHaveCatsEager =
this.people.anyMatch(person -> person.hasPet("🐱"));
Assertions.assertTrue(peopleHaveCatsEager);
}

@Test
@DisplayName("doAllPeopleHaveCatsRefactor 🐱?")
public void doAllPeopleHaveCatsRefactor()
{
boolean peopleHaveCatsLazy =
this.people.stream().allMatch(person -> person.hasPet("🐱"));

Assertions.assertFalse(peopleHaveCatsLazy);

boolean peopleHaveCatsEager =
this.people.allMatch(person -> person.hasPet("🐱"));

Assertions.assertFalse(peopleHaveCatsEager);
}

@Test
@DisplayName("getPeopleWithCatsRefactor 🐱?")
public void getPeopleWithCatsRefactor()
{
MutableList<Person> peopleWithCats =
this.people.filter(person -> person.hasPet("🐱"));

Assertions.assertEquals(2, peopleWithCats.size());
}

@Test
@DisplayName("getPeopleWithoutCatsRefactor 🐱?")
public void getPeopleWithoutCatsRefactor()
{
MutableList<Person> peopleWithoutCats =
this.people.filterNot(person -> person.hasPet("🐱"));

Assertions.assertEquals(6, peopleWithoutCats.size());
}
}

Exercise3Test

public class Exercise3Test extends PetDomainForKata
{
@Test
public void getCountsByPetEmojis()
{
MutableList<PetType> petTypes =
this.people.flatMap(Person::pets).map(Pet::type);

Map<String, Long> petEmojiCounts =
petTypes.stream()
.collect(Collectors.groupingBy(Object::toString,
Collectors.counting()));

var expectedMap =
Map.of("🐱", 2L, "🐶", 2L, "🐹", 2L, "🐍", 1L, "🐢", 1L, "🐦", 1L);
Assertions.assertEquals(expectedMap, petEmojiCounts);

MutableBag<String> counts =
this.people.countByEach(Person::getPetEmojis);

var expected =
MutableBag.of("🐱", "🐱", "🐶", "🐶", "🐹", "🐹", "🐍", "🐢", "🐦");
Assertions.assertEquals(expected, counts);
}

@Test
public void getPeopleByLastName()
{
MutableListMultimap<String, Person> lastNamesToPeople =
this.people.groupBy(Person::lastName);

Assertions.assertEquals(3, lastNamesToPeople.get("Smith").size());
}

@Test
public void getPeopleByTheirPetTypes()
{
MutableListMultimap<PetType, Person> petTypesToPeople =
this.people.groupByEach(Person::getPetTypes);

Assertions.assertEquals(2, petTypesToPeople.get(PetType.CAT).size());
Assertions.assertEquals(2, petTypesToPeople.get(PetType.DOG).size());
Assertions.assertEquals(2, petTypesToPeople.get(PetType.HAMSTER).size());
Assertions.assertEquals(1, petTypesToPeople.get(PetType.TURTLE).size());
Assertions.assertEquals(1, petTypesToPeople.get(PetType.BIRD).size());
Assertions.assertEquals(1, petTypesToPeople.get(PetType.SNAKE).size());
}

@Test
public void getPeopleByTheirPetEmojis()
{
MutableListMultimap<String, Person> petEmojisToPeople =
this.people.groupByEach(Person::getPetEmojis);

Assertions.assertEquals(2, petEmojisToPeople.get("🐱").size());
Assertions.assertEquals(2, petEmojisToPeople.get("🐶").size());
Assertions.assertEquals(2, petEmojisToPeople.get("🐹").size());
Assertions.assertEquals(1, petEmojisToPeople.get("🐢").size());
Assertions.assertEquals(1, petEmojisToPeople.get("🐦").size());
Assertions.assertEquals(1, petEmojisToPeople.get("🐍").size());
}
}

Whither Java Collections 2.0?

In April 2020, we gave a presentation to the Java Community Process (JCP) Executive Committee (EC) proposing the idea of creating a JSR for Collections 2.0. We built this experimental collections framework to demonstrate a potential future that is possible using all of the amazing features of the Java programming language, and lessons learned from various collections libraries. We placed the experimental code in the same module as the Deck of Cards Kata, which is a code kata which can be and has been used to compare and contrast various Java collection frameworks, as well as JVM Languages. The Java language has gotten even better since we gave this presentation, which was given about a month after Java 14 was released.

Two roads diverged in a wood, and I —
I took the one less traveled by,
And that has made all the difference.

-Robert Frost

I have abandoned pushing for a JSR for Collections 2.0. I have walked down the less travelled path, twice now! I have spent 20 years working on an open source collections library for Java. Eclipse Collections has proven itself useful in hundreds of thousands of production use cases, with a design that inspired some of the design choices we took in this experimental framework. Eclipse Collections continues to bring me joy collaborating on solving useful problems with so many developers in open source I may never meet in person. I will continue my work on Eclipse Collections, regardless of whither Java Collections eventually provide new or existing interfaces with eager iteration patterns.

I still believe there is value in providing eager iteration patterns directly on Java Collection types in the JDK. I wrote this blog so others might learn from this experiment, either for their own Java library work or perhaps even to get involved in helping drive the continued evolution of the existing Java Collections framework.

Why not just update the existing Java Collection interfaces?

If you’re wondering why methods like filter, map, and flatMap haven’t been added to java.util.Collection, java.util.List, java.util.Set, the following answer on StackOverflow might help.

The approach we took in the experimental collections framework by introducing new interfaces like RichIterable, MutableCollection, MutableList, etc. reduces the potential conflict surface area problem significantly.

One idea for evolving the existing Java Collections Framework would be to evolve the existing implementations (e.g. ArrayList, HashSet) by having them implement new interface extensions. The following diagram is one possible path for this evolution.

Evolving existing Java Collection implementations with new interface extensions

Takeaways from this blog

The Java Language evolution over the past decade has been truly amazing. Default and Static methods on Interfaces make many new designs possible for Java library developers. There are some “gotchas” out there, especially when dealing with diamond hierarchies, but the possibilities are incredible. I hope this blog demonstrates what is possible using some of the language features together.

I encourage folks to check out the code in the experiment and maybe try some experiments of their own with Covariant Return Types, Default and Static methods for Interfaces, and Sealed Classes. We all benefit when we learn new approaches to solving problems.

Thank you for reading this far! Best of luck on your journey, on whichever paths you choose!

I am the creator of and committer for the Eclipse Collections OSS project, which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

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.

Responses (1)

Write a response