Immutable Collections Factory Methods: List.of, Set.of, Map.of

Immutable collections factory methods List.of(), Set.of() and Map.of() in Java 25, with null and duplicate rules, copyOf(), toList() and Arrays.asList().

Diagram of List.of(), Set.of() and Map.of() and the exceptions they throw for mutator calls, null elements and duplicate elements or keys

The immutable collections factory methods List.of(), Set.of() and Map.of() create unmodifiable collections in one call, so no element can be added, removed or replaced afterwards, and every attempt throws an UnsupportedOperationException. They also reject null, and Set.of() and Map.of() reject duplicates.

We use them for constants and lookup tables, for default settings, for test data and for returning data that callers must not change, without the new ArrayList plus add() plus Collections.unmodifiableList() code we needed before.

The following example creates a list, a set and a map, copies a mutable list, and shows the two calls that fail.

List<String> weekend = List.of("Sat", "Sun");                        // [Sat, Sun]
Set<String> roles = Set.of("admin", "editor", "viewer");              // 3 elements, order not fixed
int roleCount = roles.size();                                        // 3
Map<Integer, String> statusText = Map.of(200, "OK", 404, "Not Found");
String notFound = statusText.get(404);                               // "Not Found"
List<String> snapshot = List.copyOf(new ArrayList<>(weekend));      // [Sat, Sun]
boolean added = weekend.add("Mon");                                  // UnsupportedOperationException
List<String> withNull = List.of("Sat", null);                       // NullPointerException

Notice that we read the set through size(), because the iteration order of Set.of() and Map.of() changes between JVM runs. We go through the rules of each method, copyOf(), the difference between unmodifiable and immutable, stream collectors, and a comparison with Arrays.asList() and Collections.unmodifiableList().

1. Rules of the Immutable Collections Factory Methods

The factory methods return objects of private JDK classes that implement the collection interfaces. All three follow the same rules for unmodifiable lists, unmodifiable sets and unmodifiable maps.

Diagram of List.of(), Set.of() and Map.of() and the exceptions they throw for mutator calls, null elements and duplicate elements or keys
All three factory methods throw UnsupportedOperationException on a change and NullPointerException for null; only Set.of() and Map.of() reject duplicates
  • Every mutator method, such as add(), remove(), set(), put(), clear() and sort(), throws an UnsupportedOperationException, even when the call would change nothing.
  • A null element, key or value throws a NullPointerException at creation.
  • A duplicate element in Set.of() or a duplicate key in Map.of() throws an IllegalArgumentException at creation.
  • A list keeps the order of the arguments. The iteration order of sets and maps is unspecified and is randomized for every JVM run.
  • The collections are serializable when all elements are serializable.
  • They are value-based, so we should not compare them with == or use them as a lock in synchronized.

The randomized order is deliberate. Code that depends on the iteration order of a Set or Map fails in testing instead of after a JDK upgrade. The methods came with Java 9 (JEP 269), and later releases added copyOf(), collectors and sequenced views, as summarized in Java new features.

APISince
List.of(), Set.of(), Map.of(), Map.ofEntries(), Map.entry()Java 9
List.copyOf(), Set.copyOf(), Map.copyOf()Java 10
Collectors.toUnmodifiableList(), toUnmodifiableSet(), toUnmodifiableMap()Java 10
Stream.toList()Java 16
reversed(), getFirst() and getLast() on these listsJava 21

2. Creating an Unmodifiable List With List.of()

The method List.of() has fixed-argument overloads for 0 to 10 elements and a varargs overload for more, so short lists are created without an extra array. The list keeps the argument order and allows duplicates.

List<String> steps = List.of("wash", "chop", "boil", "chop");       // [wash, chop, boil, chop]
String second = steps.get(1);                                         // "chop"
int firstChop = steps.indexOf("chop");                                // 1
String last = steps.getLast();                                        // "chop"
List<String> backwards = steps.reversed();                           // [chop, boil, chop, wash]
String replaced = steps.set(0, "rinse");                             // UnsupportedOperationException

Because the list rejects null, it also rejects null in queries. The calls contains(null) and indexOf(null) throw a NullPointerException instead of returning false or -1, which surprises code that worked with an ArrayList.

List<String> menu = List.of("tea", "coffee");
boolean hasNull = menu.contains(null);                        // NullPointerException
boolean safeCheck = new ArrayList<>(menu).contains(null);     // false

A primitive array passed to List.of() becomes a single element, so List.of(new int[] {1, 2}) is a List<int[]> of size 1. For numbers, we pass boxed values, such as List.of(1, 2). The method getLast() and the read-only view from reversed() come from sequenced collections.

3. Creating an Unmodifiable Set With Set.of()

The method Set.of() has the same overloads as List.of(). It treats a duplicate argument as a programming error and throws, because a literal with the same value twice is almost always a typo.

Set<String> vowels = Set.of("a", "e", "i", "o", "u");
boolean hasE = vowels.contains("e");                         // true
Set<String> typo = Set.of("a", "e", "a");                    // IllegalArgumentException: duplicate element: a
Set<String> deduped = Set.copyOf(List.of("a", "e", "a"));    // 2 elements
int dedupedSize = deduped.size();                            // 2

When the elements come from data, such as user input, we use Set.copyOf(), which keeps one of the duplicates instead of throwing. For a set with a stable order, we copy the elements into a LinkedHashSet or a TreeSet and wrap it, because Set.of() never keeps an order.

4. Creating an Unmodifiable Map With Map.of() and Map.ofEntries()

The method Map.of() takes keys and values as alternating arguments and has overloads for up to 10 pairs. There is no varargs version, because the key and value types differ, so for more pairs we use Map.ofEntries() with Map.entry(key, value).

Map<String, Integer> stock = Map.of("apple", 5, "banana", 3);
int apples = stock.get("apple");                                       // 5
Map<String, Integer> prices = Map.ofEntries(Map.entry("tea", 2), Map.entry("coffee", 3));
int coffee = prices.get("coffee");                                     // 3
Map<String, Integer> sameKey = Map.of("apple", 5, "apple", 7);         // IllegalArgumentException: duplicate key: apple
Map<String, Integer> nullValue = Map.of("apple", null);               // NullPointerException

A common place for Map.of() is a set of default settings. A web app ships with default values and lets each user override some of them, so we copy the defaults into a HashMap and change the copy, leaving the constant untouched.

Map<String, String> defaults = Map.of("theme", "light", "lang", "en");
Map<String, String> userSettings = new HashMap<>(defaults);
String oldTheme = userSettings.put("theme", "dark");                   // "light"
String effective = userSettings.get("theme");                          // "dark"
String unchanged = defaults.get("theme");                              // "light"

The article on immutable and unmodifiable maps covers the map variants in more depth, including Guava’s ImmutableMap.

5. Copying a Collection With copyOf()

The methods List.copyOf(), Set.copyOf() and Map.copyOf() create an unmodifiable snapshot of any collection. Later changes to the source do not show up in the copy, and when the source is already an unmodifiable collection from these methods, copyOf() may skip the copy, which JDK 25 does by returning the same instance.

The older Collections.unmodifiableList() works differently. It returns a read-only view of the original list, so code that holds the original list can still change what the view shows.

List<String> songs = new ArrayList<>(List.of("Intro", "Outro"));
List<String> copy = List.copyOf(songs);
List<String> view = Collections.unmodifiableList(songs);
songs.add("Bonus");
List<String> copyNow = copy;                             // [Intro, Outro]
List<String> viewNow = view;                             // [Intro, Outro, Bonus]
boolean sameInstance = List.copyOf(copy) == copy;        // true

A music app has a Playlist record that callers create from a mutable list. A compact constructor with List.copyOf() makes the record safe, because the caller’s list can change later without changing the playlist, and the accessor returns a list that nobody can modify.

record Playlist(String name, List<String> songs) {
    Playlist {
        songs = List.copyOf(songs);
    }
}
List<String> picks = new ArrayList<>(List.of("Intro"));
Playlist playlist = new Playlist("Morning", picks);
picks.add("Outro");
List<String> stored = playlist.songs();                     // [Intro]
boolean changed = playlist.songs().add("Bonus");            // UnsupportedOperationException

The same compact constructor also rejects a null list or a list with null songs, which is the behavior we want for most of the data in a Java record.

6. When an Unmodifiable List Is Not Immutable

An unmodifiable collection does not let us add, remove or replace elements, but it does nothing to the elements themselves. If the elements are mutable objects, anyone with a reference to an element can still change it, and the change is visible through the collection.

StringBuilder draft = new StringBuilder("v1");
List<StringBuilder> drafts = List.of(draft);
draft.append("-edited");
String inList = drafts.get(0).toString();                 // "v1-edited"

A collection from List.of() is truly immutable, and safe to share between threads without locking, only when its elements are immutable too. Strings, boxed numbers, records with immutable fields and the classes from immutable class design qualify, so List.of(“Sat”, “Sun”) is immutable, whereas List.of(draft) is only unmodifiable.

7. Collecting a Stream Into an Unmodifiable List

Streams have their own ways to produce unmodifiable results. The terminal operation Stream.toList(), added in Java 16, returns an unmodifiable list, and the collectors toUnmodifiableList(), toUnmodifiableSet() and toUnmodifiableMap() do the same since Java 10. They differ in one detail, as the second and fourth lines show.

List<String> upper = Stream.of("tea", "coffee").map(String::toUpperCase).toList();   // [TEA, COFFEE]
List<String> withNulls = Stream.of("tea", null).toList();                           // [tea, null]
List<String> collected = Stream.of("tea", "coffee").collect(Collectors.toUnmodifiableList());   // [tea, coffee]
List<String> rejected = Stream.of("tea", null).collect(Collectors.toUnmodifiableList());        // NullPointerException

The list from toList() accepts null elements, while the collectors follow the rules of List.of(). More variants, including Guava, are covered in collecting a stream into an immutable collection.

8. List.of() vs Arrays.asList() vs Collections.unmodifiableList()

Several JDK methods return a list that we cannot fully change, and each one draws the line in a different place. The differences matter most for set(), null and changes to the source.

BehaviorList.of()Arrays.asList()Collections.unmodifiableList()new ArrayList<>()
add(), remove()ThrowsThrowsThrowsWorks
set()ThrowsWorks, writes to the arrayThrowsWorks
null elementsNullPointerExceptionAllowedAllowedAllowed
Source changes visibleNo source, it is a copyYes, backed by the arrayYes, it is a viewNo
contains(null)Throwsfalsefalsefalse

When we need a list that we can change, we wrap any of these in new ArrayList<>(…). The details of Arrays.asList() are in Arrays.asList() vs new ArrayList(), and the exception itself is explained in UnsupportedOperationException on a list.

9. Immutable Collection FAQs

Teams that move a codebase from Arrays.asList() and Collections.unmodifiableList() to the factory methods ask about mutability, duplicates and size limits.

9.1. Is List.of() immutable?

The list is unmodifiable, and it is immutable when its elements are immutable. We cannot add, remove or replace elements, but a mutable element, such as a StringBuilder, can still change, as shown in section 6.

9.2. How do I add an element to a list created with List.of()?

We copy it into a mutable list first, with List<String> days = new ArrayList<>(List.of(“Sat”, “Sun”));, and call add() on the copy. The original list stays unchanged.

9.3. Why does Set.of() throw IllegalArgumentException?

Because the arguments contain the same element twice. The factory methods treat duplicates as an error. To remove duplicates from data, we use Set.copyOf(collection) or new HashSet<>(collection).

9.4. How do I create a Map with more than 10 entries?

We use Map.ofEntries() with one Map.entry(key, value) per pair, which takes any number of entries. To copy an existing map, we use Map.copyOf(map).

10. Conclusion

The methods List.of(), Set.of() and Map.of() create unmodifiable collections that reject null and, for sets and maps, duplicates. Every mutator throws an UnsupportedOperationException, and the iteration order of sets and maps changes from run to run.

We use copyOf() for defensive copies of data that came from outside, and Stream.toList() or the toUnmodifiable collectors at the end of a stream. Unlike Collections.unmodifiableList(), these collections are independent copies, and they are fully immutable when their elements are.

11. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi Lokesh,
    What is need of so many overloaded methods in List interface. Having method with varags parameter can take care of everything.

      • The reason is performance. We could indeed have just one varargs method that would deal with all number of parameters. The problem is that this requires the creation of an array, copying of parameters to the elements of that array and then garbage collection of the array. JEP 269, which details this feature explains that, “It is not a goal to support high-performance, scalable collections with arbitrary numbers of elements. The focus is on small collections.”

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.