Getting Started with Eclipse Collections — Part 2

Donald Raab
11 min readMar 21, 2023

--

Learn how to add items to and remove items from collections.

Photo by eswaran arulkumar on Unsplash

Getting Started with Eclipse Collections

In part 1 of this series, I explained how to download the Eclipse Collections library from Maven Central and create collections using Eclipse Collections. In part 2, I will explain how to add items to and remove items from collections.

Adding to and Removing from Collections

The most basic methods on a collection are the ones that add items to and remove items from the collection. Eclipse Collections has flexible and symmetric methods for growing and shrinking both MutableCollection and ImmutableCollection instances. Sometimes, there are type specific methods to add items to and remove items from a collection.

Mutable Collections

All MutableCollection types in Eclipse Collections, except for MutableStack, are descendants of java.util.Collection. MutableStack extends the java.lang.Iterable interface. The following class diagram shows the interesting mutable interfaces in the JDK and Eclipse Collections that define the adding and removing behavior for various Collection and Iterable types.

Mutable Collection interfaces with add/remove methods in JDK and Eclipse Collections

In the following sections, I will explain and show examples of the adding and removing methods at the various levels of the MutableCollection type hierarchy as they are introduced. Types in the the MutableCollection hierarchy extend the type java.util.Collection. This means the mutable collections in Eclipse Collections are compatible and interchangeable with JDK Collection types. I will start by explaining the methods inherited by the various JDK Collection types including java.util.Collection, java.util.List, java.util.Set.

java.util.Collection

The java.util.Collection interface in Java has add and remove methods that are the primary mechanisms for adding items to and removing items from a collection. The methods take an object as a parameter and both methods return boolean.

The return result for add is dependent on the Collection type. The general contract says the result will be true if the Collection is modified as a result of calling the add method. In the case of a List, the result will almost always be true. In the case of a Set, the result will depend on whether an item was added to the Set (true) or already existed in the Set (false). I recommend reading the JavaDoc for a particular type to understand the return result for add.

The return result for remove is consistent across Collection types. A result of true is returned if an items exists in the Collection and is removed.

The add method on Collection takes a generic type as a parameter. The remove method takes Object as a parameter. There are questions and answers on StackOverflow why remove takes Object as a parameter. It does allow the remove method to be less strict and more flexible.

There are also bulk mutation methods available on java.util.Collection. The three bulk mutation methods are addAll, removeAll, and retainAll, which all take a Collection as a parameter. There is also a special method named removeIf which takes a Predicate.

The following are examples using the mutating methods from java.util.Collection on an Eclipse Collections MutableList.

@Test
public void javaUtilCollection()
{
Collection<String> collection = Lists.mutable.empty();

// add
Assertions.assertTrue(collection.add("1"));
Assertions.assertTrue(collection.add("2"));
Assertions.assertTrue(collection.add("3"));
Assertions.assertEquals(List.of("1", "2", "3"), collection);

// remove
Assertions.assertTrue(collection.remove("2"));
Assertions.assertEquals(List.of("1", "3"), collection);
Assertions.assertFalse(collection.remove("4"));

// addAll
Assertions.assertTrue(collection.addAll(List.of("2")));
Assertions.assertEquals(List.of("1", "3", "2"), collection);
Assertions.assertTrue(collection.addAll(List.of("4", "5")));
Assertions.assertEquals(List.of("1", "3", "2", "4", "5"), collection);

// removeAll
Assertions.assertTrue(collection.removeAll(List.of("2", "4")));
Assertions.assertEquals(List.of("1", "3", "5"), collection);

// retainAll
Assertions.assertTrue(collection.retainAll(List.of("1", "3")));
Assertions.assertEquals(List.of("1", "3"), collection);

// removeIf
Assertions.assertFalse(collection.removeIf(
each -> Integer.parseInt(each) % 2 == 0));
Assertions.assertEquals(List.of("1", "3"), collection);
Assertions.assertTrue(collection.removeIf(
each -> Integer.parseInt(each) % 2 != 0));
Assertions.assertEquals(List.of(), collection);
}

java.util.List

The java.util.List interface inherits all of the adding and removing methods from java.util.Collection. List also includes an add and addAll method which allow for inserting elements at a specific index. There is also a remove method which takes an index. The remove method on java.util.List is an overload of the remove method defined on java.util.Collection. You should take care when calling remove on a java.util.List, to make sure you are calling the correct remove method. There are questions and answers on StackOverflow about the overloaded remove methods on java.util.List.

The following are examples using the mutating methods from java.util.List on an Eclipse Collections MutableList.

@Test
public void javaUtilList()
{
List<String> list = Lists.mutable.with("1", "3", "5");

// add(index, element) - Note: returns void, not boolean
list.add(1, "2");
list.add(3, "4");

var expected1 = Lists.mutable.with("1", "2", "3", "4", "5");
Assertions.assertEquals(expected1, list);

// addAll(index, collection) - Note: returns boolean
Assertions.assertTrue(list.addAll(0, List.of("0")));

var expected2 = Lists.mutable.with("0", "1", "2", "3", "4", "5");
Assertions.assertEquals(expected2, list);

// remove(index) - Note: returns the element removed
Assertions.assertEquals("0", list.remove(0));
Assertions.assertEquals("2", list.remove(1));
Assertions.assertEquals("4", list.remove(2));

var expected3 = Lists.mutable.with("1", "3", "5");
Assertions.assertEquals(expected3, list);
}

java.util.Set

The java.util.Set interface inherits all of the adding and removing methods from java.util.Collection. There are no additional adding or removing methods on java.util.Set. The return result of add on implementations of java.util.Set is slightly different than those of java.util.List. The result of calling add on java.util.List will always return true. On an implementation of java.util.Set, the return result is dependent on whether an equivalent item already existed in the Set. If an item doesn’t exist, add will return true. If an item does exist, add will return false.

The following are examples using the mutating methods from java.util.Set on an Eclipse Collections MutableSet.

@Test
public void javaUtilSet()
{
Set<String> set = Sets.mutable.empty();

// add - returns true if new element, or false if element exists
Assertions.assertTrue(set.add("1"));
Assertions.assertFalse(set.add("1"));
Assertions.assertEquals(Set.of("1"), set);
}

MutableCollection

The base class for most Mutable collections in Eclipse Collections is MutableCollection. MutableCollection extends java.util.Collection. It adds several methods for growing and shrinking a collection. First, MutableCollection adds a set of bulk mutating methods that are symmetric with the bulk mutating methods in java.util.Collection, but that take java.lang.Iterable instead of java.util.Collection. The methods are addAllIterable, removeAllIterable, retainAllIterable. There is also a removeIfWith method which will take a Predicate2 and a parameter of some type.

The following are examples using the mutating methods from MutableCollection on an Eclipse Collections MutableList.

@Test
public void mutableCollectionPart1()
{
MutableCollection<String> collection = Lists.mutable.with("1", "2", "3");

// addAllIterable
Assertions.assertTrue(
collection.addAllIterable(Lists.immutable.with("4", "5")));

var expected1 = Lists.mutable.with("1", "2", "3", "4", "5");
Assertions.assertEquals(expected1, collection);

// retainAllIterable
Assertions.assertTrue(
collection.retainAllIterable(Lists.immutable.with("4", "5")));

var expected2 = Lists.mutable.with("4", "5");
Assertions.assertEquals(expected2, collection);

// removeAllIterable
Assertions.assertFalse(
collection.removeAllIterable(Lists.immutable.with("1", "2", "3")));
Assertions.assertEquals(expected2, collection);
Assertions.assertTrue(
collection.removeAllIterable(Lists.immutable.with("5")));

var expected3 = Lists.mutable.with("4");
Assertions.assertEquals(expected3, collection);

// removeIfWith
Assertions.assertFalse(
collection.removeIfWith(Predicates2.equal(), "5"));
Assertions.assertTrue(
collection.removeIfWith(Predicates2.equal(), "4"));
Assertions.assertTrue(collection.isEmpty());
}

The other methods that MutableCollection includes for growing and shrinking a collection are with, withAll, without, and withoutAll. These methods are symmetric with add, addAll, remove, and removeAll from java.util.Collection. The difference is in the return type. The methods from java.util.Collection all return boolean. The with* methods on MutableCollection all return MutableCollection.

I wrote about the with* methods in the following blog.

The with* methods allow collection instances to become fluent builders for themselves, by allowing chained calls to add, addAll, remove, and removeAll.

The following are examples using the with* methods from MutableCollection on an Eclipse Collections MutableList.

@Test
public void mutableCollectionPart2()
{
MutableCollection<String> collection = Lists.mutable.with("1", "2", "3");

// with - returns MutableCollection
MutableCollection<String> with =
collection.with("4").with("5");

Assertions.assertSame(with, collection);
var expected1 = Lists.mutable.with("1", "2", "3", "4", "5");
Assertions.assertEquals(expected1, collection);

// withAll - returns MutableCollection
MutableCollection<String> withAll =
collection.withAll(List.of("6"));

Assertions.assertSame(withAll, collection);
var expected2 = Lists.mutable.with("1", "2", "3", "4", "5", "6");
Assertions.assertEquals(expected2, collection);

// without - returns MutableCollection
MutableCollection<String> without =
collection.without("5").without("6");

Assertions.assertSame(without, collection);
var expected3 = Lists.mutable.with("1", "2", "3", "4");
Assertions.assertEquals(expected3, collection);

// withoutAll - returns MutableCollection
MutableCollection<String> withoutAll =
collection.withoutAll(List.of("4"));

Assertions.assertSame(withAll, collection);
var expected4 = Lists.mutable.with("1", "2", "3");
Assertions.assertEquals(expected4, collection);
}

Covariant return types for with* methods

The interfaces MutableList, MutableSet, MutableBag, MutableSortedSet, MutableSortedBag, MutableBagIterable, and MutableSetIterable all override with, withAll, without, and withoutAll from MutableCollection providing a more specific return type. This is an extremely convenient and useful behavior.

The following code examples show how the with method is covariant by collection type in Eclipse Collections.

@Test
public void covariantReturnTypesForWithMethods()
{
// with - MutableList
MutableList<?> list = Lists.mutable.empty()
.with("1")
.with("2");

Assertions.assertEquals(List.of("1", "2"), list);

// with - MutableSet
MutableSet<?> set = Sets.mutable.empty()
.with("1")
.with("2");

Assertions.assertEquals(Set.of("1", "2"), set);

// with - MutableBag
MutableBag<?> bag = Bags.mutable.empty()
.with("1")
.with("2");

Assertions.assertEquals(Bags.mutable.with("1", "2"), bag);

// with - MutableSortedSet
MutableSortedSet<?> sortedSet = SortedSets.mutable.empty()
.with("1")
.with("2");

Assertions.assertEquals(SortedSets.mutable.with("1", "2"), sortedSet);

// with - MutableSortedBag
MutableSortedBag<?> sortedBag = SortedBags.mutable.empty()
.with("1")
.with("2");

Assertions.assertEquals(SortedBags.mutable.with("1", "2"), sortedBag);
}

The with* methods are extremely helpful for defining Collector implementations similar to the ones defined in the Collectors2 class in Eclipse Collections. The covariant overrides that return the specific types make it possible to simply use method references, instead of having to write out a multi-statement lambda.

The following code shows how to use the withAll method as a method reference on MutableList and MutableSet to define the combiner argument for a Collector.

@Test
public void collectorsDefinedUsingCovariantWithAllMethods()
{
var mutableListCollector = Collector.of(
Lists.mutable::empty,
MutableList::add,
MutableList::withAll);

MutableList<?> mutableList = List.of(1, 2, 3)
.stream()
.collect(mutableListCollector);

Assertions.assertEquals(List.of(1, 2, 3), mutableList);

var mutableSetCollector = Collector.of(
Sets.mutable::empty,
MutableSet::add,
MutableSet::withAll);

MutableSet<?> mutableSet = Set.of(1, 2, 3)
.stream()
.collect(mutableSetCollector);

Assertions.assertEquals(Set.of(1, 2, 3), mutableSet);
}

Note, it is not nearly as nice or concise defining a Collector for java.util.ArrayList or java.util.HashSet, because addAll returns boolean.

The following code shows how a multi-statement lambda is required to implement the combiner argument of a Collector for ArrayList and HashSet.

@Test
public void collectorsDefinedUsingAddAllMethods()
{
var arrayListCollector = Collector.of(
ArrayList::new,
List::add,
(left, right) -> {
left.addAll(right);
return left;
});

List<?> list = List.of(1, 2, 3)
.stream()
.collect(arrayListCollector);

Assertions.assertEquals(List.of(1, 2, 3), list);

var hashSetCollector = Collector.of(
HashSet::new,
Set::add,
(left, right) -> {
left.addAll(right);
return left;
});

Set<?> set = Set.of(1, 2, 3)
.stream()
.collect(hashSetCollector);

Assertions.assertEquals(Set.of(1, 2, 3), set);
}

MutableBagIterable

The interface MutableBagIterable is a parent interface of MutableBag and MutableSortedBag. In addition to the methods it inherits from java.util.Collection and MutableCollection, it also includes addOccurrences and removeOccurrences. A Bag looks like a Collection and supports the same adding and removing semantics, but internally keeps a Map of keys to counts. These two methods allow you to increment or decrement the counts directly without having to loop and call add or remove a certain number of times.

The following code example shows how to use addOccurrences and removeOccurrences on MutableBagIterable.

@Test
public void mutableBagIterable()
{
MutableBagIterable<Integer> bagIterable1 = Bags.mutable.with(1);

// addOccurences
bagIterable1.addOccurrences(2, 4);
bagIterable1.addOccurrences(3, 6);

// removeOccurences
bagIterable1.removeOccurrences(2, 2);
bagIterable1.removeOccurrences(3, 3);

Bag<Integer> expected1 = Bags.mutable.withOccurrences(1, 1, 2, 2, 3, 3);
Assertions.assertEquals(expected1, bagIterable1);
}

MutableStack

The one collection interface that does not extend java.util.Collection or MutableCollection is MutableStack. MutableStack defines push and pop for adding and removing elements in LIFO (Last-in, First-out) order. The return result of push is void, and the return result of pop is the element at the top of the stack that is removed. There are variations of pop that take a count, and that will return a List of elements popped off the stack.

The following code example shows how to use push and variations of pop on a MutableStack.

@Test
public void mutableStack()
{
MutableStack<Integer> stack = Stacks.mutable.empty();

// push
stack.push(1);
stack.push(2);
stack.push(3);

var expected1 = Stacks.mutable.with(1, 2, 3);
Assertions.assertEquals(expected1, stack);

// pop
Assertions.assertEquals(3, stack.pop());
Assertions.assertEquals(2, stack.pop());
Assertions.assertEquals(1, stack.pop());

// push
expected1.forEach(stack::push);

var expected2 = Stacks.mutable.with(3, 2, 1);
Assertions.assertEquals(expected2, stack);

// pop with count
ListIterable<Integer> popCount = stack.pop(3);

var expected3 = Lists.mutable.with(1, 2, 3);
Assertions.assertEquals(expected3, popCount);
}

Immutable Collections

The ImmutableCollection interface is the base interface for all immutable collections in Eclipse Collections with the exception of ImmutableStack. ImmutableCollection does not extend java.util.Collection, because if it did, it would essentially be a contractually mutable interface.

Immutable Collection interfaces with newWith*/newWithout* methods in Eclipse Collections

In Eclipse Collections, the ImmutableCollection interfaces are contractually immutable. This means they have no mutating methods defined on the interfaces. The implementations of ImmutableCollection are also structurally immutable.

The following blog explains how an ImmutableCollection hierarchy could be defined using Sealed Types in Java to provide the trifecta of immutability — contractual, structural, and verifiable. Eclipse Collections only supports contractual and structural immutability today.

ImmutableCollection

Adding items to or removing items from an ImmutableCollection is not possible. What is possible is copying the elements of the ImmutableCollection to a new ImmutableCollection with a new element or without an existing element. While it is possible to grow and shrink ImmutableCollection instances this way, it may not be desirable. Every call to newWith* or newWithout* will result in a copy of the ImmutableCollection being made. If a lot of adds or removes are required, it would be advisable to convert the ImmutableCollection to a MutableCollection type first and then finally call toImmutable after all of the adds and/or removes are complete.

Here are some examples of using the methods newWith, newWithAll, newWithout and newWithoutAll on an ImmutableCollection.

@Test
public void immutableCollection()
{
ImmutableCollection<Integer> collection = Lists.immutable.empty();

// newWith
ImmutableCollection<Integer> copyAdd1 = collection.newWith(1);
ImmutableCollection<Integer> copyAdd2 = copyAdd1.newWith(2);
ImmutableCollection<Integer> copyAdd3 = copyAdd2.newWith(3);

Assertions.assertEquals(Lists.immutable.with(1), copyAdd1);
Assertions.assertEquals(Lists.immutable.with(1,2), copyAdd2);
Assertions.assertEquals(Lists.immutable.with(1,2,3), copyAdd3);

// newWithout
ImmutableCollection<Integer> copyRemove3 = copyAdd3.newWithout(3);
ImmutableCollection<Integer> copyRemove2 = copyRemove3.newWithout(2);
ImmutableCollection<Integer> copyRemove1 = copyRemove2.newWithout(1);

Assertions.assertEquals(Lists.immutable.with(1,2), copyRemove3);
Assertions.assertEquals(Lists.immutable.with(1), copyRemove2);
Assertions.assertEquals(Lists.immutable.empty(), copyRemove1);

// newWithAll
ImmutableCollection<Integer> copyAddAll =
collection.newWithAll(Lists.mutable.with(1, 2, 3));

Assertions.assertEquals(Lists.mutable.with(1,2,3), copyAddAll);

// newWithoutAll
ImmutableCollection<Integer> copyRemoveAll =
copyAddAll.newWithoutAll(Lists.mutable.with(1, 2));

Assertions.assertEquals(Lists.mutable.with(3), copyRemoveAll);
}

Co-variant return types for newWith* methods

The interfaces ImmutableList, ImmutableSet, ImmutableBag, ImmutableSortedSet, ImmutableSortedBag, ImmutableBagIterable, and ImmutableSetIterable all override newWith, newWithAll, newWithout, and newWithoutAll from ImmutableCollection providing a more specific return type.

The following code examples show how the newWith method is covariant by collection type in Eclipse Collections.

@Test
public void covariantReturnTypesForNewWithMethods()
{
// newWith - ImmutableList
ImmutableList<?> list = Lists.immutable.empty()
.newWith("1")
.newWith("2");

Assertions.assertEquals(List.of("1", "2"), list);

// newWith - ImmutableSet
ImmutableSet<?> set = Sets.immutable.empty()
.newWith("1")
.newWith("2");

Assertions.assertEquals(Set.of("1", "2"), set);

// with - MutableBag
ImmutableBag<?> bag = Bags.immutable.empty()
.newWith("1")
.newWith("2");

Assertions.assertEquals(Bags.mutable.with("1", "2"), bag);

// with - MutableSortedSet
ImmutableSortedSet<?> sortedSet = SortedSets.immutable.empty()
.newWith("1")
.newWith("2");

Assertions.assertEquals(SortedSets.mutable.with("1", "2"), sortedSet);

// with - MutableSortedBag
ImmutableSortedBag<?> sortedBag = SortedBags.immutable.empty()
.newWith("1")
.newWith("2");

Assertions.assertEquals(SortedBags.mutable.with("1", "2"), sortedBag);
}

Primitive Collections

The methods for adding elements to and removing elements from primitive collections are very similar to their object collection counterparts. On mutable primitive collections, there are methods named add, addAll, remove, removeAll, with, withAll, without, withoutAll. There are covariant overrides for the with* methods. On the immutable primitive collections there are methods names newWith, newWithAll, newWithout, newWithoutAll and there are also covariant overrides for each of these methods on the various container types (List, Set, Bag, Stack).

The following are some examples of using various adding/removing methods on mutable primitive collections.

@Test
public void mutablePrimitiveCollections()
{
MutableIntSet set = IntSets.mutable.empty();

// add
Assertions.assertTrue(set.add(1));
Assertions.assertFalse(set.add(1));

// remove
Assertions.assertTrue(set.remove(1));
Assertions.assertFalse(set.remove(1));

// with
set.with(1).with(2).with(3).with(4);
MutableIntSet expected1 = IntSets.mutable.with(1, 2, 3, 4);
Assertions.assertEquals(expected1, set);

// without
set.without(2).without(4);
MutableIntSet expected2 = IntSets.mutable.with(1, 3);
Assertions.assertEquals(expected2, set);
}

The following are some examples of using various growing/shrinking methods on immutable primitive collections.

@Test
public void immutablePrimitiveCollections()
{
// newWith
ImmutableIntSet set1 = IntSets.immutable.empty()
.newWith(1).newWith(2).newWith(3).newWith(4).newWith(5);

Assertions.assertEquals(IntSets.mutable.with(1, 2, 3, 4, 5), set1);

// newWithout
ImmutableIntSet set2 = IntSets.immutable.with(1, 2, 3, 4, 5)
.newWithout(1).newWithout(3).newWithout(5);

Assertions.assertEquals(IntSets.mutable.with(2, 4), set2);

// newWithAll
ImmutableIntSet set3 = set2.newWithAll(IntSets.mutable.with(1, 3, 5));

Assertions.assertEquals(set1, set3);

// newWithoutAll
ImmutableIntSet set4 = set3.newWithoutAll(IntSets.mutable.with(1, 3, 5));

Assertions.assertEquals(set2, set4);
}

Final Thoughts

Thank you for taking the time to read this blog. I hope this will be a good reference for folks to refer to in the future. I tried to be as comprehensive as possible. In the next blog in this series, I will cover in depth how to convert between collection types using converter methods.

Enjoy!

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

--

--

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.