As a matter of Factory — Part 2 (Immutable)

In part 1 of this blog series on Collection Factories, I illustrated how the factory classes in Eclipse Collections can be used to create instances of mutable collections. In part 2, I will show how the same factory classes can be used to create immutable collections.

If you want to create immutable collections in Eclipse Collections, you would use <FactoryClass>.immutable.with(x, y, z).

ImmutableList<String> list = 
Lists.immutable.with("1", "2", "3");
ImmutableSet<String> set = 
Sets.immutable.with("1", "2", "3");
ImmutableBag<String> bag = 
Bags.immutable.with("1", "2", "3");
ImmutableMap<Integer, String> map = 
Maps.immutable.with(1, "1", 2, "2", 3, "3");

If you’d like a more concise option for creating immutable containers, you can use the Iterables class in Eclipse Collections with static imports.

ImmutableList<String> list = 
iList("1", "2", "3");
ImmutableSet<String> set = 
iSet("1", "2", "3");
ImmutableBag<String> bag = 
iBag("1", "2", "3");
ImmutableMap<Integer, String> map = 
iMap(1, "1", 2, "2", 3, "3");

The “i” methods are short for “immutable”, and the “m” methods are short for “mutable”. As mentioned in part 1, there is currently no equivalent of the Iterables class for the primitive factories.

If you want empty immutable collections, use the empty method on the immutable factory.

ImmutableList<String> list = 
Lists.immutable.empty();
ImmutableSet<String> set = 
Sets.immutable.empty();
ImmutableBag<String> bag = 
Bags.immutable.empty();
ImmutableMap<Integer, String> map = 
Maps.immutable.empty();

An empty immutable collection is a singleton.

ImmutableCollection does not extend Collection

There is important distinction between the MutableCollection and ImmutableCollection hierarchies in Eclipse Collections. The interfaces that extend ImmutableCollection do not extend java.util equivalents as their mutable counterparts do. This design decision was explained in detail in this StackOverflow post. While an ImmutableList does not extend java.util.List, every implementation of ImmutableList must extend java.util.List as depicted in the following diagram. The ImmutableCollection implementations will not have any mutating methods like add, remove, addAll, removeAll or clear, and the Iterator returned for any of these types will not support remove. The implementations must extend java.util.List however to support the equals and hashcode contracts of a List.

ImmutableList does not extend java.util.List

You cannot create the ImmutableList implementations directly, but instead must use the appropriate factory class. The factory classes do not return the implementation class type, but instead return the interface type, such as ImmutableList or ImmutableSet.

The factory classes may return an optimized container for smaller collection sizes. For instance, for ImmutableList, there are optimized implementations for empty, singleton, doubleton, tripleton and all the way to decapleton. None of these implementations require a backing array, but instead as the diagram depicts, keep direct references to their elements. For sets and maps, the optimization goes to quadrupleton (4). For Bags, there is a special optimization called ImmutableArrayBag, which will go as high as 20 elements, and then will use an ImmutableHashBag for larger collections. The following should give an indication of the potential memory savings obtained by using small immutable collections.

Memory Cost in Bytes

Type - Size: Mutable — Immutable
— — — — — — — — — — - - - - - - -
List — 0: 40 – 16
List — 1: 48 – 16
List — 2: 48 – 24
List — 3: 56 – 24
List — 4: 56 – 32
List — 5: 64 – 32
List — 6: 64 – 40
List — 7: 72 – 40
List — 8: 72 – 48
List — 9: 80 – 48
List -10: 80 – 56
List -11: 88 – 80
— — — — — — — — — —
Bag — 0 : 216 – 16
Bag — 1 : 232 – 32
Bag — 2 : 248 – 104
Bag — 3 : 264 – 136
Bag — 4 : 280 – 152
Bag — 5 : 296 – 184
— — — — — — — — — —
Set — 0 : 112 – 16
Set — 1 : 72 – 32
Set — 2 : 96 – 56
Set — 3 : 112 – 72
Set — 4 : 208 – 96
Set — 5 : 224 – 176

I used ObjectSizeCalculator.getObjectSize() to calculate the memory cost for each object. For List, I used null for the elements, to reduce the memory cost down to the data structure only. For Bag and Set I used new Object() for elements to have the smallest possible object footprint. The potential memory savings would be greater as well, if your mutable collections are not trimmed to size. I have worked in an application previously where I had millions of small sets, lists and maps. The memory savings that was achieved using these small immutable collections was substantial.

Interop with the JDK collection types

You can technically cast an ImmutableList to a java.util.List, but this would be an ugly and unsafe way of providing interop with existing libraries. Instead, there are methods available on each immutable interface that can cast the type to the appropriate JDK type for you. The following shows how you can use these methods.

List<String> list = 
Lists.immutable.with("1", "2", "3").castToList();
Set<String> set = 
Sets.immutable.with("1", "2", "3").castToSet();
Map<Integer, String> map = 
Maps.immutable.with(1, "1", 2, "2", 3, "3").castToMap();

It has always been a core design principle in Eclipse Collections to provide good interop with the existing JDK collection types.

Note: There is no castToBag method on ImmutableBag, because there is no Bag type in the JDK today.

Mutable Builders for Immutable Collections

There is another very useful factory approach for creating immutable collections. In Eclipse Collections, every mutable type has a method which can copy itself to its immutable equivalent type. The method name is toImmutable.

ImmutableList<String> list = 
Lists.mutable.with("1", "2", "3").toImmutable();
ImmutableSet<String> set = 
Sets.mutable.with("1", "2", "3").toImmutable();
ImmutableBag<String> bag = 
Bags.mutable.with("1", "2", "3").toImmutable();
ImmutableMap<Integer, String> map = 
Maps.mutable.with(1, "1", 2, "2", 3, "3").toImmutable();

This means that every mutable collection is a natural builder for its corresponding immutable type. We do not provide immutable builders to convert to any different types however. So if you wanted an ImmutableSet built from a MutableList, you would have to use one of the two approaches.

ImmutableSet<String> set1 = 
Lists.mutable.with("1", "2").toSet().toImmutable();

ImmutableSet<String> set2 =
Sets.immutable.withAll(Lists.mutable.with("1", "2"));

Object and Primitive Immutable Collections

Here are examples of all the object collection types you can create using the immutable factories.

ImmutableList<T> list = Lists.immutable.empty();
ImmutableSet<T> set = Sets.immutable.empty();
ImmutableSortedSet<T> sortedSet = SortedSets.immutable.empty();
ImmutableMap<K, V> map = Maps.immutable.empty();
ImmutableSortedMap<K, V> sortedMap = SortedMaps.immutable.empty();
ImmutableStack<T> stack = Stacks.immutable.empty();
ImmutableBag<T> bag = Bags.immutable.empty();
ImmutableSortedBag<T> sortedBag = SortedBags.immutable.empty();
ImmutableBiMap<K, V> biMap = BiMaps.immutable.empty();
ImmutableListMultimap<K, V> mm = Multimaps.immutable.list.empty();
ImmutableSetMultimap<K, V> mm = Multimaps.immutable.set.empty();
ImmutableBagMultimap<K, V> mm = Multimaps.immutable.bag.empty();

Eclipse Collections supports immutable containers for all eight of the Java primitive types as well. This provides good symmetry between the object and primitive containers. Here all all of the primitive types that can created using the immutable factories.

ImmutableIntList list = IntLists.immutable.empty();
ImmutableIntSet set = IntSets.immutable.empty();
// supports all combinations of primitives
ImmutableIntIntMap map = IntIntMaps.immutable.empty();
ImmutableIntObjectMap map = IntObjectMaps.immutable.empty();
ImmutableObjectIntMap map = ObjectIntMaps.immutable.empty();
ImmutableIntStack stack = IntStacks.immutable.empty();
ImmutableIntBag bag = IntBags.immutable.empty();

Growing and Shrinking Immutable Collections

Once you have an immutable collection created, you may want to grow or shrink it by adding or removing elements. As I mentioned before, there are no mutating methods like add, addAll, remove or removeAll on immutable collection interfaces in Eclipse Collections. This provides what I refer to as contractual immutability in addition to providing structural immutability. There are methods available that allow for safely copying and growing or shrinking immutable collections. There are methods named newWith, newWithAll, newWithout and newWithoutAll for extensions of ImmutableCollection. For ImmutableMap implementations, the methods are named newWithKeyValue, newWithAllKeyValues, newWithoutKey and newWithoutAllKeys.

ImmutableList<String> list0 = 
Lists.immutable.empty();
ImmutableList<String> list1 =
list0.newWith("1");
ImmutableList<String> list2 =
list1.newWithAll(Lists.mutable.with("2"));
ImmutableSet<String> set0 = 
Sets.immutable.empty();
ImmutableSet<String> set1 =
set0.newWith("1");
ImmutableSet<String> set2 =
set1.newWithAll(Sets.mutable.with("2"));
ImmutableMap<String, String> map0 = 
Maps.immutable.empty();
ImmutableMap<String, String> map1 =
map0.newWithKeyValue("1", "1");
ImmutableMap<String, String> map2 =
map1.newWithAllKeyValues(
Lists.mutable.with(Tuples.pair("2", "2")))

In the interest of providing good symmetry, these methods are available on the primitive containers as well.

ImmutableIntList list0 =
IntLists.immutable.empty();
ImmutableIntList list1 =
list0.newWith(1);
ImmutableIntList list2 =
list1.newWithAll(IntLists.mutable.with(2));
ImmutableIntSet set0 =
IntSets.immutable.empty();
ImmutableIntSet set1 =
set0.newWith(1);
ImmutableIntSet set2 =
set1.newWithAll(IntSets.mutable.with(2));
ImmutableIntIntMap map0 =
IntIntMaps.immutable.empty();
ImmutableIntIntMap map1 =
map0.newWithKeyValue(1, 1);

I discovered while writing this blog, that we were missing some symmetry in our immutable primitive map containers. We do not currently have newWithAllKeyValues on immutable primitive maps, so I submitted an issue request for the feature.

Summary

All you need to remember to create either mutable or immutable container types in Eclipse Collections is to remember the pattern of plural factory class names to types (List has Lists, Set has Sets, etc.). Then use the code completion of your IDE to decide whether you want immutable or mutable instances. Then you can choose either with or of depending on your method naming preference.

Eclipse Collections works with Java versions 5 though 9. So these factories can still be used today if you are working on any projects that have not yet upgraded to Java 8. If you are using a Java version before Java 8, simply use Eclipse Collections 7.1.1. If you are using Java 8 or above, you can use Eclipse Collections 8.x or the soon to be released 9.x version. There are milestone releases of 9.x available today if you would like to experiment and provide us with feedback.

For a complete reference of all factory types and how to create collection containers, you can read this section of the Eclipse Collections reference guide.