Designing Read-Only and Unmodifiable Collections in Java – Best Practices and Examples

Illustration for Designing Read-Only and Unmodifiable Collections in Java – Best Practices and Examples
By Last updated:

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
  • Java 9
    • Factory methods: List.of(), Set.of(), Map.of() for immutable collections
  • Java 10
    • copyOf() and Collectors.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() or Set.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() or copyOf()
  • 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

  1. Is Collections.unmodifiableList() immutable?
    No — it reflects changes in the original list.

  2. Can I add null to List.of()?
    No — throws NullPointerException.

  3. Are unmodifiable collections thread-safe?
    Only if the underlying collection is also not mutated.

  4. What’s the difference between List.copyOf() and Collections.unmodifiableList()?
    copyOf() creates a new unmodifiable list; unmodifiableList() is a view.

  5. How do I make a truly immutable map?
    Use Map.of() or Map.copyOf() (Java 9+).

  6. What happens if I call remove() on an unmodifiable collection?
    It throws UnsupportedOperationException.

  7. Are immutable collections serializable?
    Yes — but avoid mixing mutable and immutable objects.

  8. What if the elements themselves are mutable?
    Collection is unmodifiable, but element state can still change.

  9. Can I override add() to prevent modification?
    Only if you subclass a collection, but prefer composition/wrapping.

  10. Should I always use unmodifiable collections in public APIs?
    Yes — it protects encapsulation and avoids external side effects.