Back to Immutable Collections
Q2Question 2 of 3

Why does List.of() throw UnsupportedOperationException?

Key Concepts
  • List.of() returns an immutable list where add(), set(), remove(), sort(), and clear() all throw UnsupportedOperationException.
  • This is by design: immutable collections guarantee thread safety, safe sharing, and protection against accidental modification.
  • List.copyOf(collection) creates an immutable snapshot from any collection, protecting against changes to the original.
  • If the source is already an immutable List.of() result, List.copyOf() returns the same instance (no copy needed).
  • UnsupportedOperationException is a runtime exception, so callers should check the collection type or handle it appropriately.
  • To convert an immutable list to a mutable one, wrap it: new ArrayList<>(List.of("A", "B")).
  • In banking APIs, returning immutable collections prevents callers from corrupting internal service state.

Definition

List.of() creates an immutable list that throws UnsupportedOperationException on any mutation attempt, and List.copyOf() creates an immutable snapshot from any collection to protect against changes to the original.
  • Immutability contract – all mutating operations (add, set, remove, sort, clear, replaceAll) throw UnsupportedOperationException
  • Design intent – immutable collections are thread-safe, can be shared freely, and protect API boundaries from accidental modification
  • List.copyOf(collection) – creates an independent immutable copy; if the source is already immutable, returns the same instance
  • To mutable – wrap in new ArrayList<>(immutableList) to get a mutable copy
  • Stream integration – Stream.toList() (Java 16) and Collectors.toUnmodifiableList() (Java 10) return immutable lists directly from streams

At National Bank of India, the annual audit report is printed and sealed in a tamper-proof cover (List.of). No one can add pages, rip out pages, or rewrite entries. If someone needs to make changes, they must photocopy the report into a new binder (new ArrayList<>) and modify the copy. List.copyOf is like the vault department making a sealed duplicate of the ledger: even if the original ledger is later amended, the vault copy remains untouched.

Understanding the Concepts

Why does List.of() throw UnsupportedOperationException?

List.of() returns an immutable list. Every method that would change the list's state throws UnsupportedOperationException. This is an intentional design decision in the Java Collections Framework: immutable collections guarantee that their state never changes after creation.

JAVA
package com.bank.demo;

import java.util.List;

public class UnsupportedOperationDemo {
    public static void main(String[] args) {
        List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");

        String[] operations = {"add", "set", "remove", "sort", "clear"};
        for (String op : operations) {
            try {
                switch (op) {
                    case "add" -> accounts.add("NEW-001");
                    case "set" -> accounts.set(0, "CHANGED");
                    case "remove" -> accounts.remove(0);
                    case "sort" -> accounts.sort(null);
                    case "clear" -> accounts.clear();
                }
            } catch (UnsupportedOperationException e) {
                System.out.println(op + "() throws UnsupportedOperationException");
            }
        }

        // Read operations work perfectly
        System.out.println("get(0): " + accounts.get(0));
        System.out.println("size: " + accounts.size());
        System.out.println("contains SAV2001: " + accounts.contains("SAV2001"));
    }
}
TEXT
add() throws UnsupportedOperationException
set() throws UnsupportedOperationException
remove() throws UnsupportedOperationException
sort() throws UnsupportedOperationException
clear() throws UnsupportedOperationException
get(0): SAV2001
size: 3
contains SAV2001: true

How does List.copyOf create a safe snapshot?

List.copyOf takes any Collection and returns an immutable copy. The copy is completely independent of the source: changes to the source do not affect the copy, and the copy cannot be modified.

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.List;

public class CopyOfSnapshotDemo {
    public static void main(String[] args) {
        List<String> mutable = new ArrayList<>();
        mutable.add("SAV2001");
        mutable.add("CUR3001");

        // Create immutable snapshot
        List<String> snapshot = List.copyOf(mutable);
        System.out.println("Snapshot: " + snapshot);

        // Modify original: snapshot is unaffected
        mutable.add("FD4001");
        mutable.remove("SAV2001");
        System.out.println("Original after changes: " + mutable);
        System.out.println("Snapshot still: " + snapshot);

        // Optimisation: copyOf on already-immutable returns same instance
        List<String> immutable = List.of("A", "B");
        List<String> copy = List.copyOf(immutable);
        System.out.println("Same instance? " + (immutable == copy));
    }
}
TEXT
Snapshot: [SAV2001, CUR3001]
Original after changes: [CUR3001, FD4001]
Snapshot still: [SAV2001, CUR3001]
Same instance? true

How do you convert between mutable and immutable lists?

Converting from immutable to mutable requires wrapping in a new ArrayList. Converting from mutable to immutable uses List.copyOf or List.of.

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.List;

public class MutableImmutableConversionDemo {
    public static void main(String[] args) {
        // Immutable to mutable
        List<String> immutable = List.of("SAV2001", "CUR3001");
        List<String> mutable = new ArrayList<>(immutable);
        mutable.add("FD4001");
        System.out.println("Mutable copy: " + mutable);

        // Mutable to immutable
        List<String> snapshot = List.copyOf(mutable);
        System.out.println("Immutable copy: " + snapshot);

        // From stream (Java 16)
        List<String> filtered = mutable.stream()
            .filter(acc -> acc.startsWith("SAV"))
            .toList(); // returns immutable list
        System.out.println("Filtered: " + filtered);
    }
}
TEXT
Mutable copy: [SAV2001, CUR3001, FD4001]
Immutable copy: [SAV2001, CUR3001, FD4001]
Filtered: [SAV2001]

What is the difference between List.of and Arrays.asList for immutability?

| Feature | List.of() | Arrays.asList() | new ArrayList<>() |
|---|---|---|---|
| add/remove | UOE | UOE | Allowed |
| set() | UOE | Allowed (writes through) | Allowed |
| Backed by array | No | Yes | Independent |
| Null elements | No (NPE) | Yes | Yes |
| Truly immutable | Yes | No (set works) | No |

JAVA
package com.bank.demo;

import java.util.Arrays;
import java.util.List;

public class ListOfVsAsListDemo {
    public static void main(String[] args) {
        List<String> listOf = List.of("SAV2001", "CUR3001");
        List<String> asList = Arrays.asList("SAV2001", "CUR3001");

        // set() works on asList but not on List.of
        asList.set(0, "CHANGED");
        System.out.println("asList after set: " + asList);

        try {
            listOf.set(0, "CHANGED");
        } catch (UnsupportedOperationException e) {
            System.out.println("List.of set: UOE");
        }
    }
}
TEXT
asList after set: [CHANGED, CUR3001]
List.of set: UOE

What are common mistakes?

Mistake: Returning a mutable internal list from a service method

JAVA
// WRONG: caller can modify the service's internal state
public class AccountService {
    private final List<String> vipAccounts = new ArrayList<>(List.of("SAV2001", "CUR3001"));

    public List<String> getVipAccounts() {
        return vipAccounts; // caller can add/remove from internal list!
    }
}
JAVA
// CORRECT: return an immutable copy
public class AccountService {
    private final List<String> vipAccounts = new ArrayList<>(List.of("SAV2001", "CUR3001"));

    public List<String> getVipAccounts() {
        return List.copyOf(vipAccounts); // caller gets immutable snapshot
    }
}

Mistake: Expecting List.of to work with nulls from database results

JAVA
// WRONG: database query may return null values
String name = null; // from ResultSet
List<String> names = List.of("Rajesh", name, "Pankaj"); // NPE!
JAVA
// CORRECT: filter nulls first, or use ArrayList
import java.util.stream.Stream;
import java.util.Objects;

List<String> names = Stream.of("Rajesh", null, "Pankaj")
    .filter(Objects::nonNull)
    .toList();

What about shallow vs deep immutability?

List.of provides shallow immutability: the list itself cannot be modified, but the objects inside it can still be mutated if they are mutable.

JAVA
package com.bank.demo;

import java.util.List;

public class ShallowImmutabilityDemo {
    static class Account {
        String id;
        String status;
        Account(String id, String status) { this.id = id; this.status = status; }
        @Override public String toString() { return id + ":" + status; }
    }

    public static void main(String[] args) {
        Account acc = new Account("SAV2001", "ACTIVE");
        List<Account> list = List.of(acc);

        // Cannot add/remove from the list
        // list.add(new Account("X", "X")); // UOE

        // BUT: can mutate the object inside
        acc.status = "CLOSED";
        System.out.println("After mutation: " + list.get(0));
    }
}
TEXT
After mutation: SAV2001:CLOSED

For deep immutability, use Java records or ensure all elements are themselves immutable.

What should a senior engineer know?

  • Internal classes. List.of() with 0 elements returns a singleton empty list. With 1-2 elements, it returns List12 with direct fields (no array). With 3+ elements, it returns ListN with a compact array.
  • Stream.toList() vs Collectors.toUnmodifiableList(). Both produce immutable lists. Stream.toList() (Java 16) is shorter. Collectors.toUnmodifiableList() (Java 10) is available for older codebases.
  • Performance. Immutable lists have less overhead than ArrayList: no unused capacity, no size tracking, no structural modification counter.
  • Set.copyOf and Map.copyOf. The copyOf pattern applies to all three collection types.

What are common interview follow-up questions?

  • "Which operations throw UnsupportedOperationException on List.of?" : All mutating operations: add, set, remove, sort, clear, replaceAll, addAll, removeAll, retainAll.
  • "Is the UnsupportedOperationException checked or unchecked?" : Unchecked (runtime exception).
  • "How do you make a mutable copy of an immutable list?" : new ArrayList<>(immutableList).
  • "What does List.copyOf do if the source is already immutable?" : Returns the same instance (no copy).

Visual Overview

DIAGRAM
  Immutability Spectrum
  =====================

  new ArrayList<>():
  ┌────────────────────────────┐
  │ add: OK                     │
  │ set: OK                     │
  │ remove: OK                  │
  │ Fully mutable               │
  └────────────────────────────┘

  Arrays.asList(arr):
  ┌────────────────────────────┐
  │ add: UOE (fixed size)       │
  │ set: OK (writes to array)   │
  │ remove: UOE (fixed size)    │
  │ Semi-mutable                │
  └────────────────────────────┘

  Collections.unmodifiableList(list):
  ┌────────────────────────────┐
  │ add: UOE                    │
  │ set: UOE                    │
  │ remove: UOE                 │
  │ BUT: original can change!   │
  │ Unmodifiable VIEW           │
  └────────────────────────────┘

  List.of() / List.copyOf():
  ┌────────────────────────────┐
  │ add: UOE                    │
  │ set: UOE                    │
  │ remove: UOE                 │
  │ No backing mutable ref      │
  │ TRULY IMMUTABLE             │
  └────────────────────────────┘

Key Takeaway

List.of() creates truly immutable lists where every mutating operation throws UnsupportedOperationException. This is not a limitation but a design feature: immutable collections are thread-safe, can be shared freely across method boundaries, and protect internal service state from accidental modification.

List.copyOf creates an immutable snapshot from any collection, making it the standard tool for defensive copies. If the source is already an immutable list, copyOf returns the same instance, avoiding unnecessary allocation. Converting back to a mutable list is straightforward: wrap in new ArrayList<>().

At National Bank of India, Pankaj mandated that every public service method return List.copyOf for list responses. This prevented a class of bugs where a downstream consumer accidentally added or removed elements from a list that was still being used by the service internally. In a banking system, such accidental mutation could mean a missing account in an audit trail.

Remember one sentence: List.of() is truly immutable and throws UnsupportedOperationException on all mutations, while List.copyOf() creates an independent immutable snapshot, making both essential for safe API design.

---

Share
Use ← → arrow keys to navigate