Skip to content

# Part 11 — Immutable Collections (Java 9+)

This is a common topic in modern Java interviews, especially for Java 11, 17, and 21.

Typical questions:

  • What is the difference between immutable and unmodifiable collections?
  • What are List.of(), Set.of(), and Map.of()?
  • Why were immutable collection factories introduced in Java 9?
  • What is defensive copying?
  • Why are immutable collections thread-safe?

1. Why Immutable Collections?

Before Java 9:

List<String> list = new ArrayList<>();

list.add("Java");
list.add("Spring");

If you wanted a read-only view:

List<String> readOnly =
    Collections.unmodifiableList(list);

This works, but has an important limitation we'll discuss shortly.

Java 9 introduced dedicated immutable collection factories.


2. List.of()

Example:

List<String> languages =
    List.of("Java", "Spring", "Kafka");

Properties:

  • Immutable
  • Cannot add elements
  • Cannot remove elements
  • Cannot replace elements
  • Does not allow null

Trying to modify it:

languages.add("Python");

Throws:

UnsupportedOperationException

3. Set.of()

Example:

Set<String> skills =
    Set.of("Java", "Spring", "Docker");

Properties:

  • Immutable
  • No duplicate elements
  • No null

Duplicate example:

Set.of("Java", "Java");

Throws:

IllegalArgumentException

Unlike HashSet, duplicates are rejected during creation.


4. Map.of()

Example:

Map<Integer, String> map =
    Map.of(
        1, "Java",
        2, "Spring",
        3, "Kafka"
    );

Properties:

  • Immutable
  • No duplicate keys
  • No null keys
  • No null values

5. Why null Is Not Allowed

Example:

List.of("Java", null);

Throws:

NullPointerException

Reason:

Immutable collections favor predictable behavior and avoid ambiguity around null values.


6. Immutable vs Unmodifiable

This is one of the most frequently asked interview questions.


Unmodifiable Collection

List<String> original =
    new ArrayList<>();

original.add("Java");

List<String> readOnly =
    Collections.unmodifiableList(original);

Trying:

readOnly.add("Spring");

Throws:

UnsupportedOperationException

But:

original.add("Spring");

Now:

System.out.println(readOnly);

Output:

[Java, Spring]

Why?

readOnly is only a view of the original list.

The underlying list is still mutable.


Immutable Collection

List<String> list =
    List.of("Java", "Spring");

No underlying mutable list is exposed.

Contents never change.


7. Visual Comparison

Unmodifiable:

Original List

↓

Mutable

↓

Unmodifiable View

Changes to the original are visible through the view.


Immutable:

Immutable Object

↓

No modification possible

No external mutation can affect it.


8. Defensive Copying

Suppose:

class Student {

    private List<String> subjects;

    Student(List<String> subjects) {
        this.subjects = subjects;
    }
}

Problem:

subjects.add("Hacking");

The caller modifies internal state.


Better:

this.subjects =
    List.copyOf(subjects);

Now the object's internal list cannot be modified through the original reference.


9. List.copyOf()

Example:

List<String> source =
    new ArrayList<>();

source.add("Java");

List<String> copy =
    List.copyOf(source);

copy is immutable.

Changing source later does not affect copy.


10. Set.copyOf()

Set<String> set =
    Set.copyOf(existingSet);

Returns an immutable set.


11. Map.copyOf()

Map<Integer,String> copy =
    Map.copyOf(existingMap);

Returns an immutable map.


12. Why Defensive Copying?

Suppose a REST service receives:

List<Order> orders

If you store the reference directly:

this.orders = orders;

The caller can modify your internal state after construction.

Safer:

this.orders = List.copyOf(orders);

This is a common pattern in immutable classes.


13. Thread Safety

Immutable collections are naturally thread-safe.

Example:

List<String> config =
    List.of("A", "B", "C");

100 threads can read:

config.get(0);

No synchronization is required because the data never changes.


14. Performance

Immutable collections have several benefits:

  • No resize operations
  • No synchronization
  • No structural modification bookkeeping
  • Reduced implementation overhead

The JDK can also use specialized compact implementations for small immutable collections.


15. Memory Optimization

For example:

List.of("Java")

The JDK uses specialized internal classes for small immutable collections instead of a general-purpose ArrayList.

This reduces memory usage.


16. Collections.unmodifiableList()

Important distinction:

List<String> list =
    new ArrayList<>();

List<String> view =
    Collections.unmodifiableList(list);

View:

Read-only interface

Underlying list:

Still mutable

17. Immutable Class Example

public final class Employee {

    private final List<String> skills;

    public Employee(List<String> skills) {
        this.skills = List.copyOf(skills);
    }

    public List<String> getSkills() {
        return skills;
    }
}

Advantages:

  • Internal state cannot be modified externally.
  • No defensive copy is needed in the getter because the stored list is already immutable.

18. Factory Methods Summary

Method Immutable Allows Null Allows Duplicates
List.of() Yes No Yes
Set.of() Yes No No
Map.of() Yes No No duplicate keys

19. Map.ofEntries()

Useful for many entries.

Map<Integer, String> map =
    Map.ofEntries(
        Map.entry(1, "Java"),
        Map.entry(2, "Spring"),
        Map.entry(3, "Kafka")
    );

More readable than a very long Map.of(...).


20. Time Complexity

Immutable collections generally preserve the same lookup characteristics as their mutable counterparts for common operations like get() or contains(), but all structural modification methods throw UnsupportedOperationException.


Production Examples

Application Configuration

List<String> supportedCurrencies =
    List.of("USD", "EUR", "INR");

Configuration should not change at runtime.


HTTP Headers

Map<String, String> headers =
    Map.of(
        "Content-Type", "application/json",
        "Accept", "application/json"
    );

Domain Model

public Order(List<Item> items) {
    this.items = List.copyOf(items);
}

Protects internal state from callers.


Common Interview Questions

Q1. Difference between immutable and unmodifiable?

Immutable Unmodifiable
Cannot change at all Read-only view
Safe from external modification Underlying collection may still change

Q2. Why introduce List.of()?

To provide concise, memory-efficient immutable collections without requiring wrapper methods.


Q3. Can immutable collections contain null?

No.

List.of(), Set.of(), and Map.of() reject null.


Q4. Difference between List.of() and Arrays.asList()?

List.of() Arrays.asList()
Immutable Fixed-size
No null Allows null
add, remove, set all fail add/remove fail, set succeeds

Example:

List<String> list = Arrays.asList("A", "B");
list.set(0, "X"); // Works
list.add("C");    // Fails

Q5. Difference between List.copyOf() and Collections.unmodifiableList()?

List.copyOf() creates an immutable collection.

Collections.unmodifiableList() creates a read-only view over another collection.


Q6. Are immutable collections thread-safe?

Yes.

Because they cannot be modified after creation.


Q7. Why use defensive copying?

To prevent callers from modifying an object's internal state.


Q8. What happens with duplicate keys in Map.of()?

Throws:

IllegalArgumentException

Q9. Which is better for immutable domain objects?

List.copyOf()

rather than storing a mutable list reference.


Q10. Does Collections.unmodifiableList() make the original list immutable?

No.

It only prevents modification through the wrapper reference.


Senior Interview Scenario

Question:

Why is this class not truly immutable?

class Employee {

    private final List<String> skills;

    Employee(List<String> skills) {
        this.skills = skills;
    }

    public List<String> getSkills() {
        return skills;
    }
}

Answer:

The constructor stores the caller's mutable list directly. If the caller later modifies that list, the Employee object changes as well.

Correct approach:

this.skills = List.copyOf(skills);

Now the internal state cannot be changed externally.


Key Takeaways

  • List.of(), Set.of(), and Map.of() create immutable collections.
  • Immutable collections reject null; Set.of() and Map.of() also reject duplicates.
  • Collections.unmodifiableList() creates a read-only view, not an immutable collection.
  • List.copyOf() is the preferred way to perform defensive copying.
  • Immutable collections are naturally thread-safe for concurrent reads.
  • Defensive copying is a key practice when designing immutable classes and APIs.

Next: Part 12 — Collections Utility Class Deep Dive

We'll cover:

  • Collections.sort()
  • binarySearch()
  • reverse()
  • shuffle()
  • rotate()
  • swap()
  • frequency()
  • min() / max()
  • synchronizedCollection()
  • unmodifiableCollection()
  • Production use cases and interview questions