Immutable lists in Eclipse Collections

Yuri Mednikov
11 min readDec 7, 2020

Lists are by far among most common data structures, used in Java programming. In a computer science, list is a finite sequence of elements, where each element has it position (index), which allows to access it. The common list implementation in Java is an array based list — the collection, which stores entities in a backed array. While vanilla Java permits to work with immutable lists (since Java 9), it is much better experience to use third-party libraries, that are built using functional programming concepts to handle operations with immutable data structures.

Eclipse Collections library have an extensive support for both mutable and immutable collections, yet, throughout this guide we will concentrate entirely on immutable lists.

Create immutable lists

Eclipse Collections permits us to create both mutable and immutable collections, although in this series we focus only on immutable data structures. An another important thing to mention here is that Eclipse Collections distinguishes between primitive collections and object collections (you may remember, that Java collections use wrapper objects to store primitive elements). Primitive collections store elements with arrays of primitives (e.g. int[]), rather Object[], as it is done in Java.

In order to create new immutable lists we use Lists.immutable factory methods and there are two options:

  • Lists.immutable.of() = accepts a sequence of elements to populate a list
  • Lists.immutable.empty() = create an empty list

For primitive lists we use the corresponding object <type>Lists – for example, to create a list that holds integer values, use IntLists.immutable.<factory_method> pattern. Take a look on the code snippet below, which demonstrates a creation of immutable lists in Eclipse Collections:

@Test
void createListsTest(){
// create lists of objects
ImmutableList<String> names = Lists.immutable.of("Anna", "Beata", "Carol", "Denisa", "Esmeralda");
Assertions.assertThat(names).hasSize(5);
ImmutableList<String> empty = Lists.immutable.empty();
Assertions.assertThat(empty).isEmpty();
// create lists of primitives
ImmutableIntList numbers = IntLists.immutable.of(1,2,3,4,5,6,7,8,9,10);
Assertions.assertThat(numbers.size()).isEqualTo(10);
}

Besides this, the ImmutableList object can be created from the Java list. To do it, use the static factory method ofAll(), which accepts any Java iterable as an argument. Consider the following example:

@Test
void createFromJavaListTest() {
List<String> names = List.of("Anna", "Beata", "Carol", "Denisa", "Esmeralda");
ImmutableList<String> immutableNames = Lists.immutable.ofAll(names);
Assertions.assertThat(immutableNames).containsAll(names);
}

Finally, you can utilize a Java stream as a source of an immutable list. To demonstrate this, let me use IntStream to generate a sequence of numbers from 1 to 100 and create the ImmutableIntList from that stream:

@Test
void createFromStreamTest() {
IntStream stream = IntStream.rangeClosed(1, 100);
ImmutableIntList list = IntLists.immutable.ofAll(stream);
Assertions.assertThat(list.getFirst()).isEqualTo(1);
Assertions.assertThat(list.getLast()).isEqualTo(100);
}

The last note I would like to make in this section, is that in the last example we used helper methods getFirst() and getLast() to get extremes of the range, rather than using AssertJ to do it. Object lists in Eclipse Collections do implement java.lang.Iterable interface, however, primitive lists have PrimitiveIterable interface as the root. This contract does not implement the iterable and in this regard, does not have same functionality.

After we have obtained instances of immutable lists, let observe how to perform common list operations.

Add and remove entities

Array lists use a backed array to store their elements, so addition and removal take place in that underlaying data structure. An array has a fixed size, but in array-based lists it can shrink in order to accommodate entities. From the other side, we concentrate on immutable collections — these collections cannot be modified. From the technical point of view, this means, that add/remove operations return a new list, rather modify an existing one.

Personally, I found the Eclipse Collections naming convention less confusing, than Vavr. It uses methods newWith... and newWithout..., which are used to add elements and remove elements respectively. Their names tell us that they actually create a new list, that contain a given element or create a new list, that does not contain a given element.

Add elements

As we have seen in the previous section, addition means a creation of a new list, which contains all elements from the original collection, plus the given element(s).

You can add a single element or concate an original collection with another one. This means, that there are two main approaches to add elements in Eclipse Collections:

  • newWith(E element) = returns a copy of the original collection with an appended new element E
  • newWithAll(Iterable it) = returns a copy of original collection with appended elements from the it iterable

Let take a look on the practical examples. Consider this code snippet:

// add new element with newWith()
ImmutableList<String> names = Lists.immutable.of("Anna", "Beata", "Carol", "Denisa", "Esmeralda");
ImmutableList<String> addNames = names.newWith("Francesca");
Assertions.assertThat(names).doesNotContain("Francesca").hasSize(5);
Assertions.assertThat(addNames).contains("Francesca").hasSize(6);
// add elements from other collection
ImmutableList<String> capitals = Lists.immutable.of("Lisbon", "London", "Prague", "Madrid", "Stockholm");
List<String> bigCities = List.of("Porto", "York", "Brno", "Barcelona", "Malmo");
ImmutableList<String> europeanCities = capitals.newWithAll(bigCities);
Assertions.assertThat(europeanCities).containsAll(capitals).containsAll(bigCities);
Assertions.assertThat(capitals).doesNotContainAnyElementsOf(bigCities);

Note, that insertion operations do actually create new lists, that can be proven by the fact, that original lists (names and capitals respectively) do not change their size or include new elements.

Remove elements

An opposite functionality to an insertion as a deletion of entities from the collection. Again, because we work with immutable data structures, these methods technically return new lists, rather than modify originals.

Like with the previous case, there are two ways to do that:

  • newWithout(E e) = returns a copy of the original list, where the element E is removed
  • newWithoutAll(Iterable it) = similar to the previous one, but this method removes all elements, that are contained in the it iterable

Let take a primitive list in this example. Both primitive and object data structures in Eclipse Collections offer this functionality. Look on this code:

ImmutableIntList numbers = IntLists.immutable.of(1,2,3,4,5);
ImmutableIntList newNumbers = numbers.newWithout(5);
Assertions.assertThat(numbers.contains(5)).isTrue();
Assertions.assertThat(newNumbers.contains(5)).isFalse();

Likewise, we can test an immutability by checking, if the original numbers list does contain 5, however the newNumbers does not.

Filtering

Java lists do not have built-in filtering functionality, so we have to utilize streams for that. In the contrary, Eclipse Collections supplies two approaches to filter elements — select() and reject(). Due to the immutable nature, they filter the original list and return a new copy with/without entities, based on a logical condition (predicate).

The difference between these two approaches is following:

  • select() returns True if the element matches the predicate and it keeps that element in a new list
  • reject() returns False if the eleemnt matches the condition and it copies it to results

From the first glance it may seen sophisticated, however it provides a great code reusability — you can utilize same predicate and filter elements that match it or do not. With plain Java you have to write different conditions! It is better to evaluate it on a practical example. Imagine, that you write an utility, which takes a list of students with GPA (Grade Point Average) and filters students that have a good GPA (GPA > 4.0) and students that do not have good scores (less than 4.0). In Eclipse Collections you just need to create a predicate once and then you can reuse it:

Predicate2<Student, Double> predicate = (student, gpa) -> student.getGpa() > gpa;

This one-liner takes a Student object and validates, that the GPA value is bigger that the supplied value gpa. Now, you can use this single predicate for both situations:

  • selectWith(preducate, 4.0) = will return students that have GPA > 4.0
  • rejectWith(preducate, 4.0) = will return only objects that return False for the given logical condition, e.g. students with GPA less than 4.0!

Take a look on the code snippet below:

@Test
void filterTest() {
ImmutableList<Student> students = Lists.immutable.of(
new Student("Anna", 4.5),
new Student("Beata", 4.1),
new Student("Carolina", 3.9),
new Student("Daniela", 3.5),
new Student("Eva", 3.1),
new Student("Yuri", 2.5));
// filter with select()
ImmutableList<Student> goodStudents = students.select(student -> student.getGpa() > 4.0);
Assertions.assertThat(goodStudents).hasSize(2);
// filter with reject()
ImmutableList<Student> badStudents = students.reject(student -> student.getGpa() > 4.0);
Assertions.assertThat(badStudents).hasSize(4);
// use predicate
Predicate2<Student, Double> predicate = (student, gpa) -> student.getGpa() > gpa;
ImmutableList<Student> goodStudents2 = students.selectWith(predicate, 4.0);
ImmutableList<Student> badStudents2 = students.rejectWith(predicate, 4.0);
Assertions.assertThat(goodStudents2).hasSameElementsAs(goodStudents);
Assertions.assertThat(badStudents2).hasSameElementsAs(badStudents);
}

You can note, that I used two versions select/reject and selectWith/rejectWith. The first option takes a predicate, while the later one allows to supply parameters (like gpa). This provides a better level of code reusability.

Search for the element

In Java programming, to search means to find an index of the matching element or return -1 if the element is not presented in a collection. Obviously, this can answer on a question “Does a collection have an element?”, yet it requires developers to posses that element first. In real life, the search operation is used to query the collection for the element, that matches some condition. Consider, how it is performed when you search for data in a database.

Eclipse Collections bring detect operations to search for data in a collection. As these functions accept predicates, they can be employed to implement a correct repository pattern (the one, which takes criteria conditions).

Comparable to other methods, that we review here, the detect operation also have several versions:

  • detect(predicate) = the basic form, that accepts a logical condition (predicate) and returns a first matching element
  • detectWith(predicate, arg) = this version allows to pass additional argument to a predicate and in this way to reuse code

An important thing to mention here is that the detect() returns first matching element. In the case, when it could not find any element, that matches a predicate, the method returns null value. The downside, is that you need to perform an explicit null checking, that could be overcomed if Eclipse Collections would use a concept of Optional. The alternative solution is to provide a default value, that can be obtained using the supplier function, which is called, if no element in the collection does satisfy a predicate condition.

Let have an example to observe how to search for an element using Eclipse Collections immutable lists. Imagine, that we have a list of cities with their population and would like to search for the first city, that has a population (or overcome it), specified by a predicate. Take a look on the code snippet below:

@Test
void searchTest() {
ImmutableList<City> cities = Lists.immutable.of(
new City("Jihlava", 51216),
new City("Tartu", 92972),
new City("Syzran", 178750),
new City("Poznan", 534813)
);
Predicate2<City, Integer> predicate = (city, population) -> city.getPopulation() > population; City bigCity = cities.detectWith(predicate, 100000);
Assertions.assertThat(bigCity.getName()).isEqualTo("Syzran");
City noCity = cities.detectWith(predicate, 1000000);
Assertions.assertThat(noCity).isNull();
City veryBigCity = cities.detectWithIfNone(predicate, 1000000, () -> new City("Prague", 1324277));
Assertions.assertThat(veryBigCity.getName()).isEqualTo("Prague");
}

Please note, that for the noCity query, we can’t find in a list a city with population over 1 000 000 people. In that case, the detect() method returns null, which may require us to have a manual null checking. This violates principles of functional programming, as we may need to have imperative code snippet to check that (although, we can use Optional.ofNullable(), yet it can be considered a bit ugly code style too). In this situation, however we can pass a supplier function, that can be used to generate a default value.

Group elements

This is a quite common task to allocate elements into groups based on some criteria. From a technical point of view, the result is a map, where key is a criteria and a value is a list of values, that satisfy this condition.

Consider an accounting application, where you need to group invoices by customer or by issued date. Java implements this functionality as part of Stream API. In Eclipse Collections framework it can be performed using groupBy method, that accepts an aggregator function, which describes a condition to group elements. First, let start with group by customer name. For an example Invoice model, customer is just a plain string, so we can do it like this:

ImmutableList<Invoice> invoices = Lists.immutable.of(
new Invoice("Acme Co", 10000.0, LocalDate.parse("2020-01-10")),
new Invoice("Betarts s.r.o", 35000.0, LocalDate.parse("2020-01-25")),
new Invoice("ZenSolutions srl", 29990.0, LocalDate.parse("2020-02-23")),
new Invoice("Acme Co", 45000.0, LocalDate.parse("2020-03-11")),
new Invoice("Wizardworld AS", 5000.0, LocalDate.parse("2020-03-21")),
new Invoice("Karmarts AS", 7999.0, LocalDate.parse("2020-04-02")),
new Invoice("ZenSolutions srl", 8500.99, LocalDate.parse("2020-03-10"))
);
// group by company name
Multimap<String, Invoice> invoicesByCompany = invoices.groupBy(Invoice::getCompany);
Assertions.assertThat(invoicesByCompany.get("Acme Co")).hasSize(2);
Assertions.assertThat(invoicesByCompany.get("ZenSolutions srl")).hasSize(2);

We can pass a method reference as an argument for the groupBy() method. An another case is to group invoices by month. The issued date is represented by a LocalDate instance. So we may write an aggregator function, which get a month of an invoice’s issued date as a grouping criteria. Consider the following code snippet below:

// group by month
Function<Invoice, Month> invoicesAggregator = i -> i.getIssuedDate().getMonth();
Multimap<Month, Invoice> invoicesByMonth = invoices.groupBy(invoicesAggregator);
Assertions.assertThat(invoicesByMonth.get(Month.JANUARY)).hasSize(2);
Assertions.assertThat(invoicesByMonth.get(Month.MARCH)).hasSize(3);

As you can see, mutable collections make very straightforward to aggregate elements into groups, based on the specified criteria. Here we obtain a map result. It is also a common requirement to collect elements into an another collection — we will review it in the next section.

Collect

Often, developers need to gather (collect) elements of one list into another. Let quickly return to the GPA example. There we have a list of students, where each Student model takes two fields (name and GPA). Suppose, that we want to get some GPA statistics for our class, like the lowest, the highest and the average grade. We can do it with the original collection – yet, that will be much more simple to get just a list of GPAs and manipulate double values.

In order to achieve this goal, we can collect GPA values into a new primitive list using the collect...() functionality. There is a built-in collectDouble() method, which gathers elements of the list into an immutable list of double primitives.

@Test
void collectTest(){
ImmutableList<Student> students = Lists.immutable.of(
new Student("Anna", 4.5),
new Student("Beata", 4.1),
new Student("Carolina", 3.9),
new Student("Daniela", 3.5),
new Student("Eva", 3.1),
new Student("Yuri", 2.5));
// collect to list of GPAs
DoubleFunction<Student> gpaFunction = student -> student.getGpa();
ImmutableDoubleList gpas = students.collectDouble(gpaFunction);
// find some stat for class
System.out.println(String.format("The average GPA in class is %.1f", gpas.average()));
System.out.println(String.format("The lowest GPA in class is %.1f", gpas.min()));
System.out.println(String.format("The highest GPA in class is %.1f", gpas.max()));
}

We need to pass a double function, which basically maps a Student to a double value. After executing this code, we will get simple statistics for the class:

Match

An another common scenario is to validate, that elements of a sequence satisfy a logical condition. We may need to check that either all elements, either some elements, either none of elements are valid based on a some predicate If you are familiar with Java streams, you remember, that there are three methods — allMatch(), anyMatch() and noneMatch(). Eclipse Collections framework has three same methods:

  • anySatisfy()
  • allSatisfy()
  • noneSatisfy()

The principle of usage is similar to how it is done in plain Java — all we need is a predicate. Let take a list of integers in order to assert the values as even numbers. We can take two scenarios in this example:

  1. Check whenever all numbers are even (using allSatisfy() method)
  2. Check if at least one number is even (using anySatisfy())

All these methods return a boolean value.

@Test
void matchesTest() {
ImmutableList<Integer> numbers = Lists.immutable.of(10, 53, 23, 95, 30);
boolean allEven = numbers.allSatisfy(number -> number %2 == 0);
Assertions.assertThat(allEven).isFalse();
boolean atLeastOneEven = numbers.anySatisfy(number -> number %2 == 0);
Assertions.assertThat(atLeastOneEven).isTrue();
}

Conclusion

Out of the box, Java includes an array list implementation as a part of Java Collections Framework. Yet it includes a functionality for common needs, developers may found it insufficient, due to mutability or a weak support of functional API (you need to work with streams for that). Good and accepted by community alternatives include Vavr and Eclipse Collections. In this post we observed most common use cases for Eclipse Collections lists and how to deal with immutable array lists in functional way.

--

--