Back to Stream Practice: Employee Analysis
Q10Question 10 of 43

Collect employees into an unmodifiable list using toUnmodifiableList or collectingAndThen with Collections.unmodifiableList

Key Concepts
  • Collectors.toUnmodifiableList() (Java 10+) returns a truly immutable list with no backing reference leaked
  • collectingAndThen(toList(), Collections::unmodifiableList) is the Java 8 portable alternative
  • Both throw UnsupportedOperationException on add, remove, set, and clear
  • toUnmodifiableList() rejects null elements and throws NullPointerException if any are present
  • Stream.toList() (Java 16+) also returns an unmodifiable list and is the most concise option
  • Returning unmodifiable collections from service methods prevents callers from mutating shared state

Definition

Collectors.toUnmodifiableList() collects stream results into an immutable list that throws UnsupportedOperationException on any modification attempt.
  • toUnmodifiableList() – (Java 10+) collects into a genuinely immutable list with no backing mutable reference.
  • collectingAndThen(toList(), Collections::unmodifiableList) – the Java 8 form that collects into a mutable ArrayList, then wraps it in an unmodifiable view.
  • Stream.toList() – (Java 16+) the shortest form: stream.toList() returns an unmodifiable list.
  • Null rejection – toUnmodifiableList() throws NullPointerException if any element is null, consistent with List.of().
  • Defensive design – returning unmodifiable lists from service methods prevents callers from mutating shared internal state.

Priya at the National Bank of India produces a finalized list of approved high-value account holders. She laminates the printout before distributing it. The laminated list is the unmodifiable collection: everyone can read it, but no one can add or remove a name. The toUnmodifiableList() method is the lamination step.

Understanding the Concepts

What does this question ask?

The interviewer wants you to produce an immutable list from a stream. This tests knowledge of Collectors.toUnmodifiableList, collectingAndThen, Stream.toList, and the differences between them.

How do you collect into an unmodifiable list with Java 10+?

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.Collectors;

public class UnmodifiableListDemo {
    public static void main(String[] args) {
        List<Employee> employees = Employee.getSampleEmployees();

        // Java 10+: toUnmodifiableList()
        List<String> names = employees.stream()
            .map(Employee::getName)
            .collect(Collectors.toUnmodifiableList());

        System.out.println("Names: " + names);

        // This throws UnsupportedOperationException:
        try {
            names.add("Rajesh");
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify: " + e.getClass().getSimpleName());
        }
    }
}
TEXT
Names: [Rajesh, Priya, Amit, Kavita, Suresh, Deepak, Neha, Vikram, Ananya, Meera]
Cannot modify: UnsupportedOperationException

How do you do it in Java 8 with collectingAndThen?

JAVA
// Java 8 portable form
List<String> namesJava8 = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList));

// Same result: add/remove/set/clear all throw UnsupportedOperationException

collectingAndThen first collects into a mutable ArrayList, then applies the finisher function Collections::unmodifiableList which wraps it in a read-only view.

How does Stream.toList() work (Java 16+)?

JAVA
// Java 16+: shortest form
List<String> namesJava16 = employees.stream()
    .map(Employee::getName)
    .toList();  // returns unmodifiable list

// Equivalent to collect(Collectors.toUnmodifiableList()) in behavior

How are the three approaches different?

| Feature | toUnmodifiableList() | collectingAndThen | Stream.toList() |
|---|---|---|---|
| Java version | 10+ | 8+ | 16+ |
| Null elements | Throws NPE | Allows nulls | Allows nulls |
| Backing reference | None (truly immutable) | Wrapper over ArrayList | None (truly immutable) |
| Conciseness | Medium | Verbose | Most concise |
| Type | Unmodifiable List | Unmodifiable view | Unmodifiable List |

JAVA
// toUnmodifiableList: rejects null
try {
    List<String> withNull = Stream.of("Rajesh", null, "Priya")
        .collect(Collectors.toUnmodifiableList());  // NPE!
} catch (NullPointerException e) {
    System.out.println("Null rejected by toUnmodifiableList");
}

// Stream.toList: allows null
List<String> withNullOk = Stream.of("Rajesh", null, "Priya")
    .toList();  // [Rajesh, null, Priya] - works

What are common mistakes?

Mistake 1: assuming Collectors.toList() returns an unmodifiable list.

JAVA
// WRONG assumption: toList() returns a MUTABLE ArrayList
List<String> mutable = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toList());
mutable.add("NewName");  // works! list is mutable

// CORRECT: use toUnmodifiableList() for immutability
List<String> immutable = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toUnmodifiableList());
immutable.add("NewName");  // UnsupportedOperationException

Mistake 2: confusing Stream.toList() (Java 16) with Collectors.toList() (Java 8).

JAVA
// Stream.toList() -> unmodifiable
List<String> unmod = employees.stream().map(Employee::getName).toList();

// Collectors.toList() -> mutable
List<String> mod = employees.stream().map(Employee::getName)
    .collect(Collectors.toList());

// They look similar but have different mutability guarantees!

When should you use unmodifiable lists?

  • Return values from service methods: prevents callers from corrupting shared state.
  • Configuration lists: loaded once, read many times, should never change.
  • Audit trails: once recorded, entries must not be added or removed.
  • Thread safety: an unmodifiable list is safe to share across threads without synchronization.
JAVA
package com.bank.service;

import java.util.*;
import java.util.stream.Collectors;

public class ComplianceService {
    private final List<Employee> employees;

    public ComplianceService(List<Employee> employees) {
        this.employees = List.copyOf(employees);  // defensive copy
    }

    // Returns unmodifiable list: callers cannot corrupt internal state
    public List<Employee> getHighValueAccountHolders() {
        return employees.stream()
            .filter(e -> e.getSalary().doubleValue() > 1_000_000)
            .collect(Collectors.toUnmodifiableList());
    }
}

What are common interview follow-up questions?

  • "What is the difference between unmodifiable and immutable?" An unmodifiable view can still change if the backing collection changes. A truly immutable list (from toUnmodifiableList or List.of) has no backing reference.
  • "How do you create an unmodifiable Set or Map?" Use toUnmodifiableSet() or toUnmodifiableMap().
  • "What if you need to modify the list later?" Collect with Collectors.toList() to get a mutable ArrayList.

Visual Overview

DIAGRAM
  Stream<Employee>
       │
       │  .map(Employee::getName)
       ▼
  Stream<String>
       │
       │  .collect(toUnmodifiableList())
       ▼
  ┌─────────────────────────────────────────┐
  │  Unmodifiable List<String>              │
  │  [Rajesh, Priya, Amit, Kavita, ...]     │
  │                                         │
  │  .add(x)    → UnsupportedOperationEx    │
  │  .remove(x) → UnsupportedOperationEx    │
  │  .set(i, x) → UnsupportedOperationEx    │
  │  .clear()   → UnsupportedOperationEx    │
  │  .get(i)    → OK (read allowed)         │
  │  .size()    → OK (read allowed)         │
  └─────────────────────────────────────────┘

Key Takeaway

Collecting a stream into an unmodifiable list prevents callers from adding, removing, or replacing elements after collection. On Java 10+, Collectors.toUnmodifiableList() produces a truly immutable list with no backing reference. On Java 8, collectingAndThen(toList(), Collections::unmodifiableList) wraps a mutable ArrayList in a read-only view. On Java 16+, Stream.toList() is the most concise option.

The key distinction is that toUnmodifiableList() rejects null elements (throws NullPointerException), while Stream.toList() and the collectingAndThen form allow nulls. The common mistake is assuming Collectors.toList() returns an unmodifiable list. It returns a mutable ArrayList.

In banking systems, returning unmodifiable lists from service methods is a defensive design practice. The compliance list of high-value account holders flows through audit, notification, and reporting components. Making it unmodifiable prevents any downstream component from accidentally adding or removing entries.

Remember one sentence: toUnmodifiableList makes the collected result read-only and null-free, while Stream.toList (Java 16+) is shorter but allows nulls.

---

Share
Use ← → arrow keys to navigate