DEV Community

Aman
Aman

Posted on

Stop Mixing Up Unmodifiable Views and Immutable Lists

In Java, the word immutable gets thrown around too casually. That is fine until you deal with shared state, concurrency, or domain objects that must behave predictably. At that point, confusing a read-only view with a truly immutable collection becomes a real problem.

Two APIs usually sit at the center of this confusion:

  • Collections.unmodifiableList()
  • List.copyOf()

They look similar on the surface, but internally they behave very differently.


1. Collections.unmodifiableList(): Read-Only, Not Immutable

Collections.unmodifiableList() does not create a new list. It wraps an existing one.

Think of it as a guardrail. It stops callers from modifying the list through that reference and nothing more.

What it actually does

  • Wraps an existing List
  • Implements the List interface
  • Throws UnsupportedOperationException for mutation methods like add, remove, and set
  • Continues to point to the same underlying data

That last point is the important one.

List<String> internalData = new ArrayList<>(List.of("A", "B"));
List<String> publicView = Collections.unmodifiableList(internalData);

internalData.add("C");
// publicView now contains "C"
Enter fullscreen mode Exit fullscreen mode

No rules were broken.

The wrapper behaved exactly as designed.

Why this matters

  • The data is still mutable
  • Any code holding the original list can change it
  • All "unmodifiable" views will reflect those changes

When this is acceptable

  • You want to protect a method boundary
  • You fully own the backing list
  • The list will never be mutated again

When it is a bad idea

  • Shared state
  • Cached data
  • Configuration
  • Multi-threaded access

Verdict: This protects the API, not the data. It does not give you immutability, and it does not make your code thread-safe.


2. List.copyOf(): A Real Snapshot

List.copyOf() was added in Java 10 and solves the problem properly.

Instead of wrapping, it copies.

What it actually does

  • Creates a new list instance
  • Copies all elements into a private internal structure
  • Removes any connection to the source list
List<String> internalData = new ArrayList<>(List.of("A", "B"));
List<String> frozenData = List.copyOf(internalData);

internalData.add("C");
// frozenData is still ["A", "B"]
Enter fullscreen mode Exit fullscreen mode

Once created, the list cannot change, no matter what happens to the original.

Why this matters

  • No shared mutable state
  • Safe to cache
  • Safe to share across threads
  • Predictable behavior

Verdict: This is actual immutability.


Key Differences That Matter in Practice

Aspect unmodifiableList() List.copyOf()
Data copy No Yes
Backed by source Yes No
Reacts to source changes Yes No
Null elements Allowed Throws NullPointerException
Thread-safe by default No Yes
Cost O(1) O(n)

A note on null

List.copyOf() (like List.of()) rejects null values:

List.copyOf(List.of("A", null)); // NullPointerException
Enter fullscreen mode Exit fullscreen mode

This is intentional. Failing early is better than chasing random NullPointerExceptions deep inside business logic.


A Practical Rule of Thumb

Ask yourself these questions:

  1. Is this list cached, shared, or read by multiple threads? Use List.copyOf().
  2. Am I returning internal state from a class? Use List.copyOf() to avoid representation exposure.
  3. Am I only trying to stop callers from modifying a list I fully control? Collections.unmodifiableList() may be acceptable if you truly understand the risk.

The cost of copying is almost always insignificant compared to the cost of debugging race conditions.

Bottom Line

  • Unmodifiable protects the interface
  • Immutable protects the data

Most modern Java systems need the second one.
If correctness matters, stop wrapping and start copying.

Top comments (0)