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)
or <FactoryClass>.immutable.of(x, y, z)
, based on your preference for with
or of
.
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
.
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.
Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.