5 Creating Immutable Lists, Sets, and Maps

Convenience static factory methods on the List, Set, and Map interfaces, which were added in JDK 9, let you easily create immutable lists, sets, and maps.

An object is considered immutable if its state cannot change after it is constructed. After you create an immutable instance of a collection, it holds the same data as long as a reference to it exists.

If the collections created using these methods contain immutable objects, then they are automatically thread safe after construction. Because the structures do not need to support mutation, they can be made much more space efficient. Immutable collection instances generally consume much less memory than their mutable counterparts.

As discussed in About Immutability, an immutable collection can contain mutable objects, and if it does, the collection is neither immutable nor thread safe.

Use Cases

The common use case for the immutable methods is a collection that is initialized from known values, and that never changes. Also consider using these methods if your data changes infrequently.

For optimal performance, the immutable collections store a data set that never changes. However, you may be able to take advantage of the performance and space-saving benefits even if your data is subject to change. These collections may provide better performance than the mutable collections, even if your data changes occasionally.

If you have a large number of values, you may consider storing them in a HashMap. If you are constantly adding and removing entries, then this is a good choice. But, if you have a set of values that never change, or rarely change, and you read from that set a lot, then the immutable Map is a more efficient choice. If the data set is read frequently, and the values change only rarely, then you may find that the overall speed is faster, even when you include the performance impact of destroying and rebuilding an immutable Map when a value changes.

Syntax

The API for these new collections is simple, especially for small numbers of elements.

Immutable List Static Factory Methods

The List.of static factory methods provide a convenient way to create immutable lists.

A list is an ordered collection, where duplicate elements are typically allowed. Null values are not allowed.

The syntax of these methods is:

List.of()
List.of(e1)
List.of(e1, e2)         // fixed-argument form overloads up to 10 elements
List.of(elements...)   // varargs form supports an arbitrary number of elements or an array

Example 5-1 Examples

In JDK 8:

List<String> stringList = Arrays.asList("a", "b", "c");
stringList = Collections.unmodifiableList(stringList);

In JDK 9:

List stringList = List.of("a", "b", "c");

See Immutable List Static Factory Methods.

Immutable Set Static Factory Methods

The Set.of static factory methods provide a convenient way to create immutable sets.

A set is a collection that does not contain duplicate elements. If a duplicate entry is detected, then an IllegalArgumentException is thrown. Null values are not allowed.

The syntax of these methods is:

Set.of()
Set.of(e1)
Set.of(e1, e2)         // fixed-argument form overloads up to 10 elements
Set.of(elements...)   // varargs form supports an arbitrary number of elements or an array

Example 5-2 Examples

In JDK 8:

Set<String> stringSet = new HashSet<>(Arrays.asList("a", "b", "c"));
stringSet = Collections.unmodifiableSet(stringSet);

In JDK 9:

Set<String> stringSet = Set.of("a", "b", "c");

See Immutable Set Static Factory Methods.

Immutable Map Static Factory Methods

The Map.of and Map.ofEntries static factory methods provide a convenient way to create immutable maps.

A Map cannot contain duplicate keys; each key can map to at most one value. If a duplicate key is detected, then an IllegalArgumentException is thrown. Null values cannot be used as Map keys or values.

The syntax of these methods is:

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)    // fixed-argument form overloads up to 10 key-value pairs
Map.ofEntries(entry(k1, v1), entry(k2, v2),...)
 // varargs form supports an arbitrary number of Entry objects or an array

Example 5-3 Examples

In JDK 8:

Map<String, Integer> stringMap = new HashMap<String, Integer>(); 
stringMap.put("a", 1); 
stringMap.put("b", 2);
stringMap.put("c", 3);
stringMap = Collections.unmodifiableMap(stringMap);

In JDK 9:

Map stringMap = Map.of("a", 1, "b", 2, "c", 3);

Example 5-4 Map with Arbitrary Number of Pairs

If you have more than 10 key-value pairs, then create the map entries using the Map.entry method, and pass those objects to the Map.ofEntries method. For example:

import static java.util.Map.entry;
Map <Integer, String> friendMap = Map.ofEntries(
   entry(1, "Tom"),
   entry(2, "Dick"),
   entry(3, "Harry"),
   ...
   entry(99, "Mathilde"));

See Immutable Map Static Factory Methods.

Creating Immutable Copies of Collections

Let’s consider the case where you create a collection by adding elements and modifying it, and then at some point, you want an immutable snapshot of that collection. Create the copy using the copyOf family of methods added in JDK 10.

For example, suppose you have some code that gathers elements from several places:    
   List<Item> list = new ArrayList<>();
   list.addAll(getItemsFromSomewhere());
   list.addAll(getItemsFromElsewhere());
   list.addAll(getItemsFromYetAnotherPlace());
It's inconvenient to create an immutable collection using the List.of method. Doing this would require creating an array of the right size, copying elements from the list into the array, and then calling List.of(array) to create the immutable snapshot. Instead, do it in one step using the copyOf static factory method:   
   List<Item> snapshot = List.copyOf(list); 

There are corresponding static factory methods for Set and Map called Set.copyOf and Map.copyOf.

If the original collection is mutable, then the copyOf method creates an immutable collection that is a copy of the original. That is, the result contains all the same elements as the original. If elements are added to or removed from the original collection, that won't affect the copy.

If the original collection is already immutable, then the copyOf method simply returns a reference to the original collection. The point of making a copy is to isolate the returned collection from changes to the original one. But if the original collection cannot be changed, there is no need to make a copy of it.

In both of these cases, if the elements are mutable, and an element is modified, that change causes both the original collection and the copy to appear to have changed.

Creating Immutable Collections from Streams

The Streams library includes a set of terminal operations known as Collectors. A Collector is most often used to create a new collection that contains the elements of the stream. Starting in JDK 10, the java.util.stream.Collectors class has Collectors that create new immutable collections from the elements of the streams.

If you want to guarantee that the returned collection is immutable, you should use one of the toUnmodifiable- collectors. These collectors are:

   Collectors.toUnmodifiableList()
   Collectors.toUnmodifiableSet()
   Collectors.toUnmodifiableMap(keyMapper, valueMapper)     
   Collectors.toUnmodifiableMap(keyMapper, valueMapper, mergeFunction)

For example, to transform the elements of a source collection and place the results into an immutable set, you can do the following:

   Set<Item> immutableSet =
      sourceCollection.stream()
                      .map(...) 
                      .collect(Collectors.toUnmodifiableSet());

These collectors are conceptually similar to their counterparts toList, toSet, and the corresponding two toMap methods, but they have different characteristics. Specifically, the toList, toSet, and toMap methods do not guarantee whether the returned collection is mutable or immutable.

Randomized Iteration Order

The iteration order for Set elements and Map keys is randomized: it is likely to be different from one JVM run to the next. This is intentional — it makes it easier for you to identify code that depends on iteration order. Sometimes dependencies on iteration order inadvertently creep into code, and cause problems that are difficult to debug.

You can see how the iteration order is the same until jshell is restarted.

jshell> Map stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {b=2, c=3, a=1}

jshell> Map stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {b=2, c=3, a=1}

jshell> /exit
|  Goodbye

C:\Program Files\Java\jdk-9\bin>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro

jshell> Map stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {a=1, b=2, c=3}

The collection instances created by the Set.of, Map.of, and Map.ofEntries methods are the only ones whose iteration orders are randomized. The iteration ordering of collection implementations such as HashMap and HashSet is unchanged.

About Immutability

The collections returned by the convenience factory methods added in JDK 9 are conventionally immutable. Any attempt to add, set, or remove elements from these collections causes an UnsupportedOperationException to be thrown.

These collections are not "immutable persistent" or "functional" collections. If you are using one of those collections, then you can modify it, but when you do, you are returned a new updated collection that may share the structure of the first one.

One advantage of an immutable collection is that it is automatically thread safe. After you create a collection, you can hand it to multiple threads, and they will all see a consistent view.

However, an immutable collection of objects is not the same as a collection of immutable objects. If the contained elements are mutable, then this may cause the collection to behave inconsistently or make its contents to appear to change.

Let’s look at an example where an immutable collection contains mutable elements. Using jshell, create two lists of String objects using the ArrayList class, where the second list is a copy of the first. Trivial jshell output was removed.

jshell> List<String> list1 = new ArrayList<>();
jshell> list1.add("a")
jshell> list1.add("b")
jshell> list1
list1 ==> [a, b]

jshell> List<String> list2 = new ArrayList<>(list1);
list2 ==> [a, b]

Next, using the List.of method, create ilist1 and ilist2 that point to the first lists. If you try to modify ilist1, then you see an exception error because ilist1 is immutable. Any modification attempt throws an exception.

jshell> List<List<String>> ilist1 = List.of(list1, list1);
ilist1 ==> [[a, b], [a, b]]

jshell> List<List<String>> ilist2 = List.of(list2, list2);
ilist2 ==> [[a, b], [a, b]]

jshell> ilist1.add(new ArrayList<String>())
|  java.lang.UnsupportedOperationException thrown:
|        at ImmutableCollections.uoe (ImmutableCollections.java:70)
|        at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections
.java:76)
|        at (#10:1)

But if you modify the original list1, ilist1 and ilist2 are no longer equal.

jshell> list1.add("c")
jshell> list1
list1 ==> [a, b, c]
jshell> ilist1
ilist1 ==> [[a, b, c], [a, b, c]]

jshell> ilist2
ilist2 ==> [[a, b], [a, b]]

jshell> ilist1.equals(ilist2)
$14 ==> false

Immutable and Unmodifiable Are Not the Same

The immutable collections behave in the same way as the Collections.unmodifiable... wrappers. However, these collections are not wrappers — these are data structures implemented by classes where any attempt to modify the data causes an exception to be thrown.

If you create a List and pass it to the Collections.unmodifiableList method, then you get an unmodifiable view. The underlying list is still modifiable, and modifications to it are visible through the List that is returned, so it is not actually immutable.

To demonstrate this behavior, create a List and pass it to Collections.unmodifiableList. If you try to add to that List directly, then an exception is thrown.

jshell> List<String> unmodlist1 = Collections.unmodifiableList(list1);
unmodlist1 ==> [a, b, c]

jshell> unmodlist1.add("d")
|  java.lang.UnsupportedOperationException thrown:
|        at Collections$UnmodifiableCollection.add (Collections.java:1056)
|        at (#17:1)

But, if you change the original list1, no error is generated, and the unmodlist1 list has been modified.

jshell> list1.add("d")
$19 ==> true
jshell> list1
list1 ==> [a, b, c, d]

jshell> unmodlist1
unmodlist1 ==> [a, b, c, d]

Space Efficiency

The collections returned by the convenience factory methods are more space efficient than their mutable equivalents.

All of the implementations of these collections are private classes hidden behind a static factory method. When it is called, the static factory method chooses the implementation class based on the size. The data may be stored in a compact field-based or array-based layout.

Let’s look at the heap space consumed by two alternative implementations. First, here’s an unmodifiable HashSet that contains two strings:
Set<String> set = new HashSet<>(3);   // 3 buckets
set.add("silly");
set.add("string");
set = Collections.unmodifiableSet(set);
The set includes six objects: the unmodifiable wrapper; the HashSet, which contains a HashMap; the table of buckets (an array); and two Node instances (one for each element). On a typical VM, with a 12–byte header per object, the total overhead comes to 96 bytes + 28 * 2 = 152 bytes for the set. This is a large amount of overhead compared to the amount of data stored. Plus, access to the data unavoidably requires multiple method calls and pointer dereferences.

Instead, we can implement the set using Set.of:

Set<String> set = Set.of("silly", "string");

Because this is a field-based implementation, the set contains one object and two fields. The overhead is 20 bytes. The new collections consume less heap space, both in terms of fixed overhead and on a per-element basis.

Not needing to support mutation also contributes to space savings. In addition, the locality of reference is improved, because there are fewer objects required to hold the data.