Java Immutable vs Unmodifiable Collection: Key Differences
java immutable vs unmodifiable collection: Understand the difference between immutable and unmodifiable collections in Java, including behavior, performance, and when...
The distinction between a java immutable vs unmodifiable collection is often misunderstood, yet it directly affects correctness and safety. An unmodifiable collection is a view over a mutable backing collection, while an immutable collection is a snapshot that cannot change. This article explains the practical difference and how to choose the right one.
What Unmodifiable Means in Java
An unmodifiable collection is a wrapper that prevents modification through the wrapper's reference. The underlying collection remains mutable. If you hold a reference to the original list, you can still change it, and those changes are visible through the unmodifiable view.
List<String> original = new ArrayList<>(); original.add("one"); List<String> unmodifiable = Collections.unmodifiableList(original); unmodifiable.add("two"); // throws UnsupportedOperationException original.add("three"); // allowed, and unmodifiable now contains three
This behavior is intentional. The wrapper delegates read operations to the backing collection but blocks write operations.
What Immutable Means in Java
An immutable collection cannot be changed after creation, and it has no backing mutable source. In Java, List.of, Set.of, and Map.of create truly immutable collections. They reject null values and do not support modification. Because there is no backing collection, the data is fixed at creation time.
List<String> immutable = List.of("one", "two"); immutable.add("three"); // throws UnsupportedOperationException
The key difference is that immutable is a snapshot, while unmodifiable is a view.
Creating Unmodifiable Collections with Collections.unmodifiableList
Collections.unmodifiableList wraps an existing List. It returns a view that disallows structural changes. The wrapper implements the List interface, so it can be passed to methods that expect a List. However, any changes to the original list are reflected in the wrapper.
List<String> source = new ArrayList<>(); source.add("a"); List<String> view = Collections.unmodifiableList(source); System.out.println(view); // [a] source.add("b"); System.out.println(view); // [a, b]
This is useful when you need to expose a collection without letting callers modify it, but you still need to update the collection internally.
Creating Immutable Collections with List.of and Set.of
List.of and Set.of were introduced in Java 9. They create fixed-size collections that cannot be modified. They also disallow null elements. The returned collection is not a view; it owns its data.
List<String> immutable = List.of("a", "b"); Set<Integer> numbers = Set.of(1, 2, 3);
These collections are also more memory-efficient than a wrapper around an ArrayList because they store elements in a compact internal representation.
Behavior When the Underlying Collection Changes
The most important practical difference is what happens when the original collection is modified. With an unmodifiable view, the view reflects those changes. This can lead to surprising bugs if the view is shared across threads or if the original collection is modified after the view is created.
List<String> shared = new ArrayList<>(); shared.add("x"); List<String> readOnly = Collections.unmodifiableList(shared); // Another thread modifies shared shared.add("y"); // readOnly now contains both x and y
Immutable collections do not have this problem because they have no backing collection. Once created, their content is fixed.
Defensive Copying and Immutability
When you receive a collection from untrusted code or from a constructor, you often need to protect your internal state. Using an unmodifiable view of the passed collection does not protect you if the caller keeps a reference and modifies it. You need a defensive copy.
public class Registry { private final List<String> names; public Registry(List<String> names) { this.names = List.copyOf(names); // immutable copy } }
List.copyOf creates an immutable copy, which is safer than Collections.unmodifiableList in this scenario.
Performance and Memory Considerations
Immutable collections created by List.of have a smaller memory footprint than a wrapper around a mutable list because they do not need an extra wrapper object or a separate backing array. They also avoid the overhead of checking for concurrent modification during iteration. Unmodifiable views add one layer of indirection for every read operation, but the difference is usually negligible unless the collection is accessed extremely frequently.
The bigger cost is when you use an unmodifiable view but the underlying collection changes. That can cause ConcurrentModificationException during iteration if the backing list is modified by another thread. Immutable collections are inherently thread-safe because they cannot be modified.
Choosing Between Immutable and Unmodifiable
Use an unmodifiable view when you need to expose a read-only interface to a collection that you still control and modify internally. Use an immutable collection when you want to guarantee that the data never changes, especially when sharing it across threads or storing it in a field that must remain stable.
If you are copying data into a new collection, prefer List.copyOf or Set.copyOf over wrapping a mutable list with Collections.unmodifiableList.
Common Pitfalls and Edge Cases
One common mistake is assuming that Collections.unmodifiableList makes the elements themselves immutable. It does not. If the list contains mutable objects, those objects can still be changed through their references. The same is true for List.of; the collection is immutable, but the objects it holds are not.
Another pitfall is using Arrays.asList with Collections.unmodifiableList. Arrays.asList returns a fixed-size list backed by the array, but the array can still be changed. Wrapping it in an unmodifiable view does not prevent changes to the array.
Finally, remember that List.of and Set.of reject null elements. If your data may contain null, you need to use Collections.unmodifiableList or a copy that allows nulls, such as new ArrayList<>(source).