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.

- 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.
| API | Since |
|---|---|
| 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 lists | Java 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.
| Behavior | List.of() | Arrays.asList() | Collections.unmodifiableList() | new ArrayList<>() |
|---|---|---|---|---|
| add(), remove() | Throws | Throws | Throws | Works |
| set() | Throws | Works, writes to the array | Throws | Works |
| null elements | NullPointerException | Allowed | Allowed | Allowed |
| Source changes visible | No source, it is a copy | Yes, backed by the array | Yes, it is a view | No |
| contains(null) | Throws | false | false | false |
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
- Java 25 API, List, Unmodifiable Lists
- Java 25 API, Set, Unmodifiable Sets
- Java 25 API, Map, Unmodifiable Maps
- Java 25 Core Libraries, Creating Unmodifiable Lists, Sets, and Maps
- JEP 269, Convenience Factory Methods for Collections
Happy Learning !!
Hi Lokesh,
What is need of so many overloaded methods in List interface. Having method with varags parameter can take care of everything.
In fact, I also find it weird. I will try to find more reasoning behind it.
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.”
Thank you for jumping into the discussion. Yes, you are right about improved performance of small collections. For others to read more, please go to JEP 269.