In Java, read-only and unmodifiable collections are crucial for achieving immutability, security, and thread safety. Whether you are exposing data from an API, designing a configuration object, or working in a multi-threaded environment, making collections unmodifiable helps prevent accidental or malicious mutations.
But not all read-only collections are created equal. Java provides different ways to create them, each with subtle differences. This tutorial will explain what they are, how to implement them, and which one to use depending on your scenario.
What Are Read-Only and Unmodifiable Collections?
- A read-only collection means clients can view but not modify the data.
- An unmodifiable collection throws
UnsupportedOperationException
if modification methods are called. - An immutable collection is deeply unchangeable — its elements and structure can’t be modified.
Why Use Unmodifiable Collections?
- To enforce immutability and prevent state corruption
- For defensive programming — safe API boundaries
- In multi-threaded applications to avoid race conditions
- For safe return values that should not be altered
Common Java Methods for Unmodifiable Collections
1. Collections.unmodifiableXXX()
(Java 1.2+)
List<String> list = new ArrayList<>();
List<String> unmodifiable = Collections.unmodifiableList(list);
- Creates a wrapper — changes to the original list are reflected.
- Shallowly read-only.
2. List.of()
, Set.of()
, Map.of()
(Java 9+)
List<String> immutableList = List.of("a", "b", "c");
- Truly immutable, fixed-size, no nulls allowed.
UnsupportedOperationException
for any mutation.
3. Stream.collect(Collectors.toUnmodifiableList())
(Java 10+)
List<String> list = Stream.of("x", "y").collect(Collectors.toUnmodifiableList());
- Clean and safe way to collect immutable collections from streams.
4. Defensive Copying
public class Config {
private final List<String> servers;
public Config(List<String> servers) {
this.servers = List.copyOf(servers); // Java 10+
}
public List<String> getServers() {
return servers; // Already unmodifiable
}
}
Real-World Use Cases
- Exposing data in REST APIs without risk of alteration
- Returning config values to avoid mutations
- Sharing collections across threads without locking
- Using DTOs with immutable lists for UI frameworks
Code Example: Read-Only Wrappers
List<String> original = new ArrayList<>(List.of("A", "B"));
List<String> readonly = Collections.unmodifiableList(original);
original.add("C");
System.out.println(readonly); // [A, B, C]
readonly.add("D"); // Throws UnsupportedOperationException
Performance Considerations
Method | Overhead | Thread-Safe | Notes |
---|---|---|---|
Collections.unmodifiableList() |
Low | No | Wrapper only; original is mutable |
List.of() |
Very Low | Yes | Immutable, fixed-size |
copyOf() |
Medium | Yes | Makes defensive copy |
Collectors.toUnmodifiableList() |
Medium | Yes | Best for streams |
Java Version Tracker
📌 What's New in Java?
- Java 8
- Functional support:
Collectors
,Predicate
,Stream
views
- Functional support:
- Java 9
- Factory methods:
List.of()
,Set.of()
,Map.of()
for immutable collections
- Factory methods:
- Java 10
copyOf()
andCollectors.toUnmodifiableList()
- Java 21
- Virtual threads encourage immutability by default for safe concurrency
Functional Programming and Unmodifiable Collections
List<String> names = List.of("Anna", "Bob", "Cara");
List<String> upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toUnmodifiableList());
- Combines immutability and clean functional style
Best Practices
- Use
List.of()
orSet.of()
for truly immutable collections - Use
Collections.unmodifiableList()
for wrapping mutable collections - Avoid returning raw mutable lists from public APIs
- Prefer
List.copyOf()
in constructors for defensive copying - Never expose modifiable fields directly
Anti-Patterns
- Wrapping a mutable list with
unmodifiableList()
and modifying the original later - Storing mutable objects inside unmodifiable collections — they can still change internally
- Assuming
unmodifiableList()
is deeply immutable (it’s not)
Refactoring Legacy Code
- Identify public methods returning
List
,Set
,Map
- Replace with
Collections.unmodifiableXXX()
orcopyOf()
- Document immutability clearly in Javadocs
- Use immutable factory methods in DTOs and models
Comparisons
Method | Immutable | Reflects Original | Null Allowed | Java Version |
---|---|---|---|---|
Collections.unmodifiableList() |
❌ No | ✅ Yes | ✅ Yes | 1.2+ |
List.of() |
✅ Yes | ❌ No | ❌ No | 9+ |
List.copyOf() |
✅ Yes | ❌ No | ❌ No | 10+ |
Collectors.toUnmodifiableList() |
✅ Yes | ❌ No | ❌ No | 10+ |
Conclusion and Key Takeaways
- Unmodifiable ≠ Immutable — wrapping doesn’t prevent mutation of the original
- Use factory methods (
List.of
) for truly immutable collections - Prefer immutable APIs for safety, thread safety, and code clarity
- Mix functional programming with immutable collectors for modern Java style
FAQ – Read-Only and Unmodifiable Collections
-
Is Collections.unmodifiableList() immutable?
No — it reflects changes in the original list. -
Can I add null to List.of()?
No — throwsNullPointerException
. -
Are unmodifiable collections thread-safe?
Only if the underlying collection is also not mutated. -
What’s the difference between List.copyOf() and Collections.unmodifiableList()?
copyOf()
creates a new unmodifiable list;unmodifiableList()
is a view. -
How do I make a truly immutable map?
UseMap.of()
orMap.copyOf()
(Java 9+). -
What happens if I call remove() on an unmodifiable collection?
It throwsUnsupportedOperationException
. -
Are immutable collections serializable?
Yes — but avoid mixing mutable and immutable objects. -
What if the elements themselves are mutable?
Collection is unmodifiable, but element state can still change. -
Can I override add() to prevent modification?
Only if you subclass a collection, but prefer composition/wrapping. -
Should I always use unmodifiable collections in public APIs?
Yes — it protects encapsulation and avoids external side effects.