Eclipse Collections is an open source Java Collections Framework which enables writing functional, fluent code in Java.
Eclipse Collections started off as a collections framework named Caramel at Goldman Sachs in 2004. Since then the framework has evolved, and in 2012, it was open sourced to GitHub as a project called GS Collections. Over the years, around 40 or so developers from the same company have contributed to the collections framework. To maximize the best nature of open source project, GS Collections was migrated to the Eclipse Foundation, re-branded as Eclipse Collections in 2015. Now the framework is fully open to the community, accepting contributions!
Eclipse Collections was designed to provide Rich, functional, fluent and fun API along with memory efficient data structures all the while providing interoperability with Java Collections. It provides missing types like
Evolution of the Framework
Over the past 14+ years, the framework has matured and the highest interface:
RichIterable now has more than 100 methods on it. These methods were included on the interface after careful deliberation. Below are the steps we take while adding an API:
1. Use case: Majority of the methods added to the framework are motivated by user requirements. Users will raise either an Issue or directly a Pull Request on the project and then we start the discussion.
2. Static Utility vs API: Eclipse Collections has static utility classes like
ListIterate, etc. These static utility classes allow us to prototype our features before adding it as an API. If the static utility methods are heavily used then in subsequent release we try to implement the method as an API on the collection interface to provide a rich and fluent coding experience.
Iterate#groupByAndCollect() is currently implemented on the static utility. Since the method is used frequently it justifies adding it as an API on
RichIterable to provide a rich, functional and fluent coding experience. There is an open issue in case you would like to help us out.
3. Covariant Overrides: We override the API methods logically such that the API returns a type which is true to it’s behavior.
RichIterable has an API called
select() which is similar to
filter() which returns all elements of the collections which evaluate true for the
Predicate. Below is how the API is defined on each interface:
// RichIterable RichIterable<T> select(Predicate<? super T> predicate); // ListIterable ListIterable<T> select(Predicate<? super T> predicate); // MutableList MutableList<T> select(Predicate<? super T> predicate)
As you can see
RichIterable returns a
ListIterable returns a
MutableList returns a
4. Overloads with Target: Sometimes it is possible that we need a different collection than the one returned. In order to make it efficient and fluent, we create an overloaded method which accepts a target collection. The target collections are used to accumulate the results and return the target collection.
For example: As described above, the
select() method on a
MutableList returns a
MutableList. However, what if you want a
MutableSet? There is an overloaded
select() method available which takes in a target collection which can be a set.
MutableList<Integer> integers = Lists.mutable.with( 1, 2, 2, 3, 3, 3, 4, 4, 4, 4); MutableList<Integer> evens = integers.select(each -> each % 2 == 0); Assert.assertEquals(Lists.mutable.with(2, 2, 4, 4, 4, 4), evens); MutableSet<Integer> uniqueEvens = integers.select( each -> each % 2 == 0, Sets.mutable.empty()); Assert.assertEquals(Sets.mutable.with(2, 4), uniqueEvens);
5. Symmetry: Eclipse Collections offers primitive collections. We try to maintain symmetry between the object collections and primitive collections to provide a complete user experience.
Implementing an API in Practice
Let us implement a simple API
RichIterable#countBy() which was added in Eclipse Collections release 9.0.0: The motivation for this API was the users mentioning having to
collect() a collection in a
Bag. In Eclipse Collections
collect() is similar to
Bag is a data structure which maintains a mapping of an object to the count.
MutableList<String> strings = Lists.mutable.with( "1", "2", "2", "3", "3", "3", "4", "4", "4", "4"); Bag<Integer> integers = strings.collect( Integer::valueOf, Bags.mutable.empty()); Assert.assertEquals(1, integers.occurrencesOf(1)); Assert.assertEquals(2, integers.occurrencesOf(2)); Assert.assertEquals(3, integers.occurrencesOf(3)); Assert.assertEquals(4, integers.occurrencesOf(4));
The above solution to count the integers worked, however, it was not intuitive. An inexperienced developer might have a hard time to implement this solution. So, we decided to add
countBy() and now the code is looks more functional, fluent and moreover intuitive.
MutableList<String> strings = Lists.mutable.with( "1", "2", "2", "3", "3", "3", "4", "4", "4", "4"); Bag<Integer> integers = strings.countBy(Integer::valueOf); Assert.assertEquals(1, integers.occurrencesOf(1)); Assert.assertEquals(2, integers.occurrencesOf(2)); Assert.assertEquals(3, integers.occurrencesOf(3)); Assert.assertEquals(4, integers.occurrencesOf(4));
In this blog I explained the evolution strategy of a mature Java collections library. The aspects we look at are use case, utility vs API, covariant overrides, necessary overloads and lastly symmetry.
It is a personal goal to get 1000 stars on our GitHub project, so, if you like the framework, show your support and put a star on the repository.