5 Creating Unmodifiable Lists, Sets, and Maps

Convenience static factory methods on the List, Set, and Map interfaces let you easily create unmodifiable lists, sets, and maps.

A collection is considered unmodifiable if elements cannot be added, removed, or replaced. After you create an unmodifiable instance of a collection, it holds the same data as long as a reference to it exists.

A collection that is modifiable must maintain bookkeeping data to support future modifications. This adds overhead to the data that is stored in the modifiable collection. A collection that is unmodifiable does not need this extra bookkeeping data. Because the collection never needs to be modified, the data contained in the collection can be packed much more densely. Unmodifiable collection instances generally consume much less memory than modifiable collection instances that contain the same data.

Use Cases

Whether to use an unmodifiable collection or a modifiable collection depends on the data in the collection.

An unmodifiable collection provides space efficiency benefits and prevents the collection from accidentally being modified, which might cause the program to work incorrectly. An unmodifiable collection is recommended for the following cases:

  • Collections that are initialized from constants that are known when the program is written
  • Collections that are initialized at the beginning of a program from data that is computed or is read from something such as a configuration file

For a collection that holds data that is modified throughout the course of the program, a modifiable collection is the best choice. Modifications are performed in-place, so that incremental additions or deletions of data elements are quite inexpensive. If this were done with an unmodifiable collection, a complete copy would have to be made to add or remove a single element, which usually has unacceptable overhead.

Syntax

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

Unmodifiable List Static Factory Methods

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

A list is an ordered collection, where duplicate elements are 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 and later:

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

See Unmodifiable Lists.

Unmodifiable Set Static Factory Methods

The Set.of static factory methods provide a convenient way to create unmodifiable 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 and later:

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

See Unmodifiable Sets.

Unmodifiable Map Static Factory Methods

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

A Map cannot contain duplicate keys. If a duplicate key is detected, then an IllegalArgumentException is thrown. Each key is associated with one value. Null cannot be used for either 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 and later:

Map<String, Integer> 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 Unmodifiable Maps.

Creating Unmodifiable 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 unmodifiable snapshot of that collection. Create the copy using the copyOf family of methods.

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 unmodifiable 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 unmodifiable 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. Because the parameter of List.copyOf and Set.copyOf is Collection, you can create an unmodifiable List that contains the elements of a Set and an unmodifiable Set that contains the elements of a List. If you use Set.copyOf to create a Set from a List, and the List contains duplicate elements, an exception is not thrown. Instead, an arbitrary one of the duplicate elements is included in the resulting Set.

If the collection you want to copy is modifiable, then the copyOf method creates an unmodifiable 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 unmodifiable, 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 Unmodifiable 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. The java.util.stream.Collectors class has Collectors that create new unmodifiable collections from the elements of the streams.

If you want to guarantee that the returned collection is unmodifiable, 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 unmodifiable set, you can do the following:

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

If the stream contains duplicate elements, the toUnmodifiableSet collector chooses an arbitrary one of the duplicates to include in the resulting Set. For the toUnmodifiableMap(keyMapper, valueMapper) collector, if the keyMapper function produces duplicate keys, an IllegalStateException is thrown. If duplicate keys are a possibility, use the toUnmodifiableMap(keyMapper, valueMapper, mergeFunction) collector instead. If duplicate keys occur, the mergeFunction is called to merge the values of each duplicate key into a single value.

The toUnmodifiable- 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 make no guarantee about the modifiablilty of the returned collection, however, the toUnmodifiable- collectors guarantee that the result is unmodifiable.

Randomized Iteration Order

Iteration order for Set elements and Map keys is randomized and likely to be different from one JVM run to the next. This is intentional and makes it easier to identify code that depends on iteration order. Inadvertent dependencies on iteration order can cause problems that are difficult to debug.

The following example shows how the iteration order is different after jshell is restarted.

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

jshell> /exit
|  Goodbye

C:\Program Files\Java\jdk\bin>jshell

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

Randomized iteration order applies to the collection instances created by the Set.of, Map.of, and Map.ofEntries methods and the toUnmodifiableSet and toUnmodifiableMap collectors. The iteration ordering of collection implementations such as HashMap and HashSet is unchanged.

About Unmodifiable Collections

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

However, 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 unmodifiable 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 unmodlist1 and unmodlist2 that point to the first lists. If you try to modify unmodlist1, then you see an exception error because unmodlist1 is unmodifiable. Any modification attempt throws an exception.

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

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

jshell> unmodlist1.add(new ArrayList<String>())
|  java.lang.UnsupportedOperationException thrown:
|        at ImmutableCollections.uoe (ImmutableCollections.java:71)
|        at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections
.java:75)
|        at (#8:1)

But if you modify the original list1, the contents of unmodlist1 changes, even though unmodlist1 is unmodifiable.

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

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

jshell> unmodlist1.equals(unmodlist2)
$14 ==> false

Unmodifiable Collections vs. Unmodifiable Views

The unmodifiable collections behave in the same way as the unmodifiable views returned by the Collections.unmodifiable... methods. However, the unmodifiable collections are not views — 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> list1 = new ArrayList<>();
jshell> list1.add("a")
jshell> list1.add("b")
jshell> list1
list1 ==> [a, b]

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

jshell> unmodlist1.add("c")
|  Exception java.lang.UnsupportedOperationException
|        at Collections$UnmodifiableCollection.add (Collections.java:1058)
|        at (#8:1)

Note that unmodlist1 is a view of list1. You cannot change the view directly, but you can change the original list, which changes the view. If you change the original list1, no error is generated, and the unmodlist1 list has been modified.

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

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

The reason for an unmodifiable view is that the collection cannot be modified by calling methods on the view. However, anyone with a reference to the underlying collection, and the ability to modify it, can cause the unmodifiable view to change.

Space Efficiency

The collections returned by the convenience factory methods are more space efficient than their modifiable 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.

Thread Safety

If multiple threads share a modifiable data structure, steps must be taken to ensure that modifications made by one thread do not cause unexpected side effects for other threads. However, because an immutable object cannot be changed, it is considered thread safe without requiring any additional effort.

When several parts of a program share data structures, a modification to a structure made by one part of the program is visible to the other parts. If the other parts of the program aren't prepared for changes to the data, then bugs, crashes, or other unexpected behavior could occur. However, if different parts of a program share an immutable data structure, such unexpected behavior can never happen, because the shared structure cannot be changed.

Similarly, when multiple threads share a data structure, each thread must take precautions when modifying that data structure. Typically, threads must hold a lock while reading from or writing to any shared data structure. Failing to lock properly can lead to race conditions or inconsistencies in the data structure, which can result in bugs, crashes, or other unexpected behavior. However, if multiple threads share an immutable data structure, these problems cannot occur, even in the absence of locking. Therefore, an immutable data structure is said to be thread safe without requiring any additional effort such as adding locking code.

A collection is considered unmodifiable if elements cannot be added, removed, or replaced. However, an unmodifiable collection is only immutable if the elements contained in the collection are immutable. To be considered thread safe, collections created using the static factory methods and toUnmodifiable- collectors must contain only immutable elements.