Back to Blog
Java

Java Collections Un UnmodifiableList: How to Create and Use

java collections unmodifiablelist: Learn how to to create unmodifiable lists in Java using Collections.unmodifiableList() and List.of(), including behavior, pitfalls,...

javamain_keyword
A Java code snippet showing an unmodifiable list being created from a mutable list, with a lock icon indicating immutability.

When you need to to expose a list without allowing callers to modify it, java collections unmodifiablelist is the standard solution. In Java, the Collections class provides unmodifiableList(), which wraps a mutable list in a read-only view. Since Java 9, List.of() offers a simpler way to create an immutable list directly. Both approaches serve different purposes, and choosing the right one depends on whether you need a view of an existing list or a fixed set of elements.

The Two Main Ways to Create an Unmodifiable List

The Java Collections Framework offers two primary ways to obtain an unmodable list. The first is Collections.unmodifiableList(List) which returns a wrapper around an existing list. The second is List.of() which creates a new immutable list from a fixed set of elements. The following example shows both:

List<String> mutableList = new ArrayList<>(); mutableList.add("apple"); mutableList.add("banana"); List<String> unmodifiableView = Collections.unmodifiableList(mutableList); List<String> immutableList = List.of("apple", "banana");

The unmodifiableView is a live view: changes to mutableList are reflected in it. The immutableList is a separate, fixed-size list that cannot be modified after creation. This distinction is central to choosing the right API.

How Collections.unmodifiableList() Works

Collections.unmodifiableList() takes a List and returns an object that implements the List interface but delegates all read operations to the backing list. Any attempt to call a mutator method such as add, remove, set, or clear on the returned list throws UnsupportedOperationException. The wrapper does not copy the elements; it simply blocks modification through the view.

List<String> names = new ArrayList<>(); names.add("Alice"); List<String> readOnly = Collections.unmodifiableList(names); readOnly.add("Bob"); // throws UnsupportedOperationException

Because the wrapper delegates to the original list, the original list remains mutable. If the original list is modified elsewhere, the unmodifiable view sees those changes. This is useful when you want to provide read-only access to an internal collection without copying it.

Behavior of the Returned List

The returned list supports all read operations: get, contains, indexOf, size, and iteration. It also supports sublist, stream, and forEach because those do not modify the list. However, any method that changes the list structure or element values will throw UnsupportedOperationException. This includes add, remove, set, clear, sort, and replaceAll. The exception is thrown at runtime, not at compile time, because the wrapper is designed to be a drop-in replacement for List.

List<String> fixed = Collections.unmodifiableList(Arrays.asList("a", "b")); System.out.println(fixed.size()); // 2 System.out.println(fixed.get(0)); // a fixed.set(0, "c"); // throws UnUnsupportedOperationException

It is important to note that the unmodifiable view does not make the elements themselves immutable. If the list contains mutable objects, those objects can still be changed through their own references. The view only prevents structural changes to the list.

When to Use Collections.unmodifiableList() vs List.of()

Choosing between the two depends on the source of the data and the the need for a live view. Use Collections.unmodifiableList() when you already have a list that you want to expose without copying, and when you want the view to reflect changes to the original list. Use List.of() when you are creating a new list from literal values or when you want a truly immutable list that cannot be changed even if the original source changes.

CriterionCollections.unmodifiableList()List.of()
Backing listWraps an existing listCreates a new list
CopyingNo copy, view of originalCopies elements into a fixed list
Mutability of originalOriginal remains mutableNo original list
Null elementsAllowed (if backing list allows)Throws NullPointerException
SizeVariable (reflects original)Fixed at creation
Java versionAvailable since Java 1.2Available since Java 9

If you need to pass a list to an API that requires a List but should not modify it, Collections.unmodifiableList() is the safe choice when the list already exists. If you are constructing a list from known values, List.of() is more concise and also provides a more compact internal representation.

Performance and Memory Considerations

Collections.unmodifiableList() adds a thin wrapper around the existing list. The wrapper does not copy the data, so it adds minimal memory overhead and negligible runtime cost for read operations. However, because the wrapper delegates to the backing list, every read operation goes through an extra method call. In practice, this overhead is tiny and rarely a bottleneck.

List.of() creates a new list that is typically backed by an array. It consumes memory proportional to the number of elements and does not depend on an existing collection. The list is also immutable, which allows the JVM to optimize storage in some cases. For small lists, List.of() can be more memory-efficient than a wrapper around a mutable list because it avoids the overhead of an ArrayList's capacity and growth logic.

Neither approach introduces thread-safety guarantees. An unmodifiable view is not automatically safe for concurrent access if the backing list is modified by another thread. List.of() returns an immutable list that is safe to share across threads because it cannot be changed, but the elements themselves may still be mutable.

Common Pitfalls and Edge Cases

One common mistake is assuming that the unmodifiable list is immutable in the sense that the elements cannot be changed. As mentioned, the wrapper only prevents structural changes. If the list contains a Person object with a setName method, you can still call that method on the object retrieved from the list.

Another pitfall is relying on the unmodifiable view when the backing list is modified. If the backing list is changed after the view is created, the view reflects those changes. This can lead to unexpected behavior if the backing list is modified concurrently or by another part of the code. For example:

List<String> mutable = new ArrayList<>(); mutable.add("a"); List<String> view = Collections.unmodifiableList(mutable); mutable.add("b"); System.out.println(view); // [a, b]

If you need a truly immutable list that does not change, use List.of() or copy the list into a new list before wrapping it with Collections.unmodifiableList().

Null handling also differs. Collections.unmodifiableList() does not check for null elements; it depends on the backing list. List.of() throws NullPointerException if any element is null. If your data may contain nulls and you need an immutable list, you cannot use List.of() directly.

Unmodifiable Lists from Streams

Since Java 10, the Collectors class provides toUnmodifiableList(), which collects stream elements into an unmodifiable list. This is convenient when you want to produce a read-only list from a stream without first collecting into a mutable list.

List<String> result = stream.map(String::toUpperCase) .collect(.toUnmodifiableList());

The returned list is similar to one created by List.of() in that it is immutable and does not allow null elements. It does not reflect any later changes to the stream source because the stream is consumed. This method is useful when you want to guarantee that the result of a stream pipeline is never modified.

When you need to pass an unmodable list to a method that expects a List, the stream collector provides a clean way to produce it without intermediate mutable collections. However, if you already have a list and simply want to to protect it, Collections.unmodifiableList() remains the appropriate tool.

java collections unmodifiablelist: Practical Usage and Code | RYUSLOG DEV