Java 9 Immutable Collections Example
Hello, in this tutorial we will see another JDK 9 feature i.e. creating immutable collections in Java. Java 9 brings the long awaited approach for creating small unmodifiable Collection instances using a concise one line code. As per JEP 269, new convenience factory methods will be included in JDK 9.
1. Introduction
Before Java 9, it was possible to create an immutable view of the collections but only with some utility methods e.g. Collections.unmodifiableCollection(Collection<? extends T> c)
. For example, let us create an immutable view of Collection in Java 8, with a one liner. It looks bad! Isn’t it?
Test.java
Map<String, String> immutableMap = Collections.unmodifiableMap(new HashMap<String, String>() {{ put("key1", "Value1"); put("key2", "Value2"); put("key3", "Value3"); }});
That’s too much code for a simple task and it should be possible to be done in a single expression. Java 9 brings now, something useful with the factory methods for creating immutable collections. Here are the examples of the factory methods:
Java Doc
// Empty Immutable Collections List emptyImmutableList = List.of(); Set emptyImmutableSet = Set.of(); Map emptyImmutableMap = Map.of(); // Immutable Collections List immutableList = List.of("one", "two"); Set immutableSet = Set.of("value1", "value2"); Map<String, String> immutableMap = Map.of("key1", "Value1", "key2", "Value2", "key3", "Value3");
2. Java 9 Immutable Collections Example
2.1 What are Collection Factory methods?
A collection factory method in Java is a static method that provides a simple way of initializing an immutable Collection<E>
.
Being immutable, no elements can be added to, removed from, or modified inside the Collection<E>
after it is initialized. With Java 9, collection factory methods are provided for the following interfaces: List<E>
, Set<E>
and Map<K, V>
2.2 How are they implemented?
A new package-private utility class that resides in the JDK 9 java.util.ImmutableCollections
, provides multiple abstract classes that each represent a base for an immutable Collection<E>
: AbstractImmutableList<E>
, AbstractImmutableSet<E>
and AbstractImmutableMap<K, V>
.
These abstract classes are used to implement four concrete classes (except for AbstractImmutableMap<K, V>
which implements three concrete classes) for each Collection<E>
:
- List<E>
List0<E>
: An immutable implementation of an emptyList<E>
List1<E>
: An immutable implementation of aList<E>
with one elementList2<E>
: An immutable implementation of aList<E>
with two elementsListN<E>
:An immutable implementation of aList<E>
with a variable amount of elements
- Set<E>
Set0<E>
: An immutable implementation of an emptySet<E>
Set1<E>
: An immutable implementation of aSet<E>
with one elementSet2<E>
: An immutable implementation of aSet<E>
with two elementsSetN<E>
: An immutable implementation of aSet<E>
with a variable amount of elements
- Map<K, V>
Map0<K, V>
: An immutable implementation of an emptyMap<K, V>
Map1<K, V>
: An immutable implementation of aMap<K, V>
with one key-value entryMapN<K, V>
: An immutable implementation of aMap<K, V>
with a variable amount of key-value entries
2.3 What do they improve?
Up until Java 9, there has been no simple universal method to initialize a Collection<E>
with initial elements/key-value entries. Previously, developers were required to initialize them as follows (assuming the generic types E, K, and V have been replaced with Integer):
- List<Integer>
- The following method is arguably the simplest to initialize a
List<Integer>
with initial elements, however the result is simply a view of aList<Integer>
. We are unable to add to or remove from thisList<Integer>
, but we are still able to modify existing elements by usingList#set
. For E.g.:List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
- If we wanted our
List<Integer>
to be entirely mutable, then we would have to pass it to the constructor of anArrayList<Integer>
, for example:List<Integer> mutableList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
- The following method is arguably the simplest to initialize a
- Set<Integer>
- A
Set<Integer>
required more code to initialize with initial elements than aList<Integer>
does, which can be seen as:Set<Integer> mutableSet = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
- A
- Map<Integer, Integer>
- A
Map<Integer, Integer>
is arguably the most complicated to initialize with initial key-value entries; however, there are multiple ways to go about it:- One method was to first initialize an empty
Map<Integer, Integer>
and simply callMap#put
to add key-value entries - Another method was to use an anonymous class with two curly braces, which would still require
Map#put
to be called
- One method was to first initialize an empty
- A
2.4 What is the proper syntax to use?
For simplicity, we’re going to look at how to create List, Set, Map with Java 9 Factory Method for Collections.
2.4.1. List
To create a List, we use below static methods:
Java Doc
// for empty list static <E> List<E> of() // for list containing one element static <E> List<E> of(E e1) // for list containing two element static <E> List<E> of(E e1, E e2) // for list containing an arbitrary number of elements static <E> List<E> of(E... elements)
For example:
Test.java
List<String> immutableList = List.of(); immutableList = List.of("one", "two", "three", null);
If we try to create list with null
element, a java.lang.NullPointerException
will be thrown:
Console Output
Exception in thread "main" java.lang.NullPointerException at java.base/java.util.Objects.requireNonNull(Objects.java:221) at java.base/java.util.ImmutableCollections$ListN.(ImmutableCollections.java:233) at java.base/java.util.List.of(List.java:859)
Because the list created with static factory method is immutable, so if we try to add an element to list, it also throws an java.lang.UnsupportedOperationException
Test.java
List<String> immutableList = List.of("one", "two", "three"); immutableList.add("four");
Console Output
Exception in thread "main" java.lang.UnsupportedOperationException at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:70) at java.base/java.util.ImmutableCollections$AbstractImmutableList.add(ImmutableCollections.java:76)
Solution for problems above:
Test.java
List<String> mutableList = new ArrayList<String>(List.of("one", "two", "three")); mutableList.add("four"); mutableList.add(null);
Console Output
// Result: [one, two, three, four, null]
2.4.2. Set
To create a Set, we use below static methods:
Java Doc
// for empty Set static <E> Set<E> of() // for Set containing one element static <E> Set<E> of(E e1) // for Set containing two element static <E> Set<E> of(E e1, E e2) // for Set containing an arbitrary number of elements static <E> Set<E> of(E... elements)
For example:
Test.java
Set<String> immutableSet = Set.of(); immutableSet = Set.of("one", "two", "three", null);
If we try to create set with null
element, a java.lang.NullPointerException
will be thrown:
Console Output
Exception in thread "main" java.lang.NullPointerException at java.base/java.util.ImmutableCollections$SetN.probe(ImmutableCollections.java:520) at java.base/java.util.ImmutableCollections$SetN.(ImmutableCollections.java:460) at java.base/java.util.Set.of(Set.java:520)
Because the set created with static factory method is immutable, so if we try to add an element to set, it also throws an java.lang.UnsupportedOperationException
Test.java
Set<String> immutableSet = Set.of("one", "two", "three"); immutableSet.add("four");
Console Output
Exception in thread "main" java.lang.UnsupportedOperationException at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:70) at java.base/java.util.ImmutableCollections$AbstractImmutableSet.add(ImmutableCollections.java:280)
Solution for problems above:
Test.java
Set<String> mutableSet = new HashSet<String>(Set.of("one", "two", "three")); mutableSet.add("four"); mutableSet.add(null);
Console Output
// Result: [null, four, one, two, three]
2.4.3. Map
Map.of()
To create a Map, we use below static methods:
Java Doc
// for empty Map static <K, V> Map<K, V> of() // for Map containing a single mapping static <K, V> Map<K, V> of(K k1, V v1) // for Map containing two mappings static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2) // for Map containing up to ten mappings static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10)
For example:
Test.java
Map<Integer, String> immutableMap = Map.of(1, "one", 2, "two", 3, "three", 4, null );
If we try to create map with null
element, a java.lang.NullPointerException
will be thrown:
Console Output
Exception in thread "main" java.lang.NullPointerException at java.base/java.util.Objects.requireNonNull(Objects.java:221) at java.base/java.util.ImmutableCollections$MapN.(ImmutableCollections.java:677) at java.base/java.util.Map.of(Map.java:1372)
Because the map created with static factory method is immutable, so if we try to put (key, value) pair to map, it also throws an java.lang.UnsupportedOperationException
Test.java
Map<Integer, String> immutableMap = Map.of(1, "one", 2, "two", 3, "three"); immutableMap.put(4, "four");
Console Output
Exception in thread "main" java.lang.UnsupportedOperationException at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:70) at java.base/java.util.ImmutableCollections$AbstractImmutableMap.put(ImmutableCollections.java:557)
Solution for problems above:
Test.java
Map<Integer, String> mutableMap = new HashMap<Integer, String>(Map.of(1, "one", 2, "two", 3, "three")); mutableMap.put(4, "four"); mutableMap.put(5, null);
Console Output
// Result: {1=one, 2=two, 3=three, 4=four, 5=null}
Map.ofEntries()
If we want to create a Map with more than ten mappings, there is another way: Using Map.ofEntries()
method.
Java Doc
static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries)
To use that method, we use a method for boxing keys and values, suitable for static import:
Java Doc
static <K, V> Entry<K, V> entry(K k, V v)
So, this is way to use them:
Test.java
Map<Integer, String> newImmutableMap = Map.ofEntries(Map.entry(1, "one"), Map.entry(2, "two"), Map.entry(3, "three"));
2.5 Can I use collection factory methods to create mutable objects?
The Collection<E>
created by collection factory methods are inherently immutable, however we are able to pass them to a constructor of an implementation of the Collection<E>
to produce a mutable version i.e.
- List<Integer>
List<Integer> mutableList = new ArrayList<>(List.of(1, 2, 3, 4, 5));
- Set<Integer>
Set<Integer> mutableSet = new HashSet<>(Set.of(1, 2, 3, 4, 5));
- Map<Integer, Integer>
Map<Integer, Integer> mutableMap = new HashMap<>(Map.of(1, 2, 3, 4));
3. Pitfalls of Java’s Immutable Collections
The danger of Java’s implementation is that because there is no interface specifically for immutable collections, those immutable Set
and Map
collections still have the mutable methods add
/put
and remove which will throw an UnsupportedOperationException
if called.
Blankly looking at of
, it isn’t obvious that the returned collection is immutable. A HashSet
would be a reasonable guess since it is by far the most widely used Java set. Java’s Comparable EnumSet.of(...)
returns a mutable set. More than a few runtime exceptions are going to be thrown due to of
ambiguous return type.
4. Conclusion
The main goal of this article is to discuss the new collection factory methods in Java 9. Of all the new features added to Java 9, the factory method of
is one of the more useful in day-to-day programming but it needs to be used with caution.
5. Download the Eclipse Project
This was an example of Java 9 Immutable Collections. Run the code and the result will be printed in the console window.
You can download the full source code of this example here: Java9 Immutable Collections