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 likeImmutableCollections.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+ orList.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
- Lambdas, Streams,
- 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.