Immutable Collections in Java 9+ – List.of() vs Collections.unmodifiableList()

Illustration for Immutable Collections in Java 9+ – List.of() vs Collections.unmodifiableList()
By Last updated:

In modern Java development, especially from Java 9 onward, immutable collections have become a core strategy for ensuring thread safety, enhancing performance, and creating safer, more predictable code. Two widely used ways to create immutable lists in Java are List.of() and Collections.unmodifiableList(). This article takes a deep dive into these approaches, helping you understand their behavior, performance, use cases, and differences across Java versions.

Core Concepts

Immutable collections are collections whose structure and contents cannot be changed once created. They are crucial in multi-threaded applications, functional programming paradigms, and modern Java design principles.

Java Syntax and Examples

List.of (Java 9+)

List<String> immutableList = List.of("Java", "Python", "Go");
immutableList.add("C++"); // Throws UnsupportedOperationException

Collections.unmodifiableList (Java 1.2+)

List<String> list = new ArrayList<>();
list.add("Java");
List<String> immutableList = Collections.unmodifiableList(list);
list.add("Python"); // Also affects immutableList — it's a view!

Internal Implementation

  • List.of() returns a truly immutable instance backed by specialized internal classes like ImmutableCollections.ListN.
  • Collections.unmodifiableList() returns a wrapper view, meaning changes to the original list reflect in the unmodifiable version.

Performance and Complexity

Operation List.of() unmodifiableList()
Read O(1) O(1)
Add/Remove Unsupported Unsupported
Memory Footprint Low (optimized) Higher (wrapper)

Real-world Use Cases

  • DTO responses in REST APIs
  • Thread-safe data sharing
  • Functional transformations using Streams

Version Differences

Java 8

  • No List.of() method
  • Use Collections.unmodifiableList()

Java 9

  • Introduced List.of(), Map.of(), Set.of()
  • Enhanced factory methods for immutability

Java 10+

  • Type inference with var for more concise code

Java 21

  • No direct changes to List.of but better performance and memory improvements under the hood

Functional Programming Support

List<String> list = List.of("a", "b", "c");
List<String> upper = list.stream()
                         .map(String::toUpperCase)
                         .collect(Collectors.toList());

Pros and Cons

List.of()

  • ✅ Compact syntax
  • ✅ Truly immutable
  • ❌ Nulls are not allowed

Collections.unmodifiableList()

  • ✅ Backward compatible
  • ❌ Still allows mutation of original list

Anti-Patterns and Fixes

  • Anti-pattern: Relying on unmodifiableList() for immutability in shared state.
  • Fix: Use List.copyOf(original) in Java 10+ or List.of() in Java 9+.

Legacy Code Refactoring

Convert:

List<String> list = new ArrayList<>();
// fill list
List<String> immutable = Collections.unmodifiableList(list);

To:

List<String> immutable = List.copyOf(list);

Best Practices

  • Prefer List.of() when creating hardcoded or fixed data sets.
  • Use List.copyOf() when converting mutable collections to immutable safely.
  • Avoid modifying the source list when using unmodifiableList().

📌 What's New in Java Versions?

  • Java 8
    • Lambdas, Streams, Collectors.toList(), Optional
  • Java 9
    • List.of(), Set.of(), Map.of(), compact immutable collection factories
  • Java 10
    • var keyword for type inference
  • Java 21
    • Performance boosts, improved memory layouts

Conclusion and Key Takeaways

Immutable collections play a vital role in modern Java applications. List.of() provides a safer and more efficient alternative to Collections.unmodifiableList(). Understanding their differences ensures better design, cleaner code, and fewer bugs.

FAQ

1. Can I add null to List.of()?

No, it throws NullPointerException.

2. Is Collections.unmodifiableList() truly immutable?

No, it’s a read-only view. Mutations to the underlying list are still possible.

3. Which one is faster?

List.of() is generally faster and consumes less memory.

4. When should I use List.copyOf()?

When you want a defensive copy of an existing collection.

5. Is List.of thread-safe?

Yes, it’s immutable and thus safe for concurrent access.

6. What happens if I modify the source list after creating unmodifiableList?

Changes are visible in the unmodifiable view.

7. Are these collections serializable?

Yes, but be cautious about compatibility across Java versions.

8. Can I stream over List.of?

Yes, just like any other List.

9. What’s the difference between List.of() and Arrays.asList()?

Arrays.asList() returns a fixed-size but mutable list backed by the array.

10. Should I always use immutable lists?

Prefer them in multi-threaded or API-boundaries where mutation is not required.