Back to Collectors
Q7Question 7 of 21

Stream.toList() (Java 16) vs collect(Collectors.toList()): unmodifiable vs mutable, breaking change

Key Concepts
  • Stream.toList() (Java 16) returns an unmodifiable list that throws on add/remove/set/sort
  • collect(Collectors.toList()) returns a mutable ArrayList that allows all mutations
  • Swapping collect(toList()) with .toList() breaks code that mutates the result
  • toList() allows null elements, while toUnmodifiableList() rejects them
  • toList() is shorter and signals read-only intent, making it the default choice for final results
  • Use collect(toList()) or new ArrayList<>(stream.toList()) when you need a mutable result

Definition

Stream.toList() returns an unmodifiable list, while collect(Collectors.toList()) returns a mutable ArrayList. Swapping one for the other breaks code that mutates the result.
  • Stream.toList() (Java 16) is a concise terminal operation that returns an unmodifiable list. Throws UnsupportedOperationException on add/remove/set/sort.
  • collect(Collectors.toList()) returns a mutable ArrayList that allows all mutations (add, remove, set, sort).
  • Breaking change: replacing collect(toList()) with .toList() during code modernization breaks any code that later mutates the result.
  • toList() allows null elements. toUnmodifiableList() rejects nulls with NPE.
  • Use .toList() for the common read-only case (clearer, less typing).
  • Use collect(toList()) when you need to modify the result afterward.

collect(toList()) gives you a DRAFT document at National Bank of India that you can edit, add pages to, and reorganize. Stream.toList() gives you a FINALIZED document that is sealed and read-only. Replacing a draft with a finalized version breaks any process that tries to edit it.

Understanding the Concepts

What is the difference?

JAVA
package com.bank.util;

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

public class ToListComparisonDemo {
    public static void main(String[] args) {
        var numbers = Stream.of(3, 1, 4, 1, 5);

        // collect(toList()) → mutable ArrayList
        List<Integer> mutable = numbers.collect(Collectors.toList());
        mutable.add(9);      // OK
        mutable.sort(null);  // OK
        System.out.println("Mutable: " + mutable);

        // Stream.toList() → unmodifiable
        List<Integer> unmod = Stream.of(3, 1, 4, 1, 5).toList();
        try {
            unmod.add(9);    // UnsupportedOperationException!
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify toList() result");
        }
    }
}
TEXT
Mutable: [1, 1, 3, 4, 5, 9]
Cannot modify toList() result

When does swapping break code?

JAVA
// Original code: works fine with mutable list
List<String> names = accounts.stream()
    .map(Account::holder)
    .collect(Collectors.toList());
names.sort(null);  // sorts the mutable list
names.add("New Account");  // adds to the mutable list

// After modernization: breaks!
List<String> names = accounts.stream()
    .map(Account::holder)
    .toList();  // unmodifiable!
names.sort(null);  // UnsupportedOperationException!

Comparison table

| Feature | collect(Collectors.toList()) | Stream.toList() | toUnmodifiableList() |
|---------|-------------------------------|-------------------|----------------------|
| Java version | 8+ | 16+ | 10+ |
| Mutability | Mutable (ArrayList) | Unmodifiable | Unmodifiable |
| Null elements | Allowed | Allowed | Rejected (NPE) |
| Encounter order | Preserved | Preserved | Preserved |
| Conciseness | Verbose | Concise | Medium |

How do you get a mutable list from toList()?

JAVA
// Wrap in a new ArrayList for mutability
List<String> mutableCopy = new ArrayList<>(accounts.stream()
    .map(Account::holder)
    .toList());
mutableCopy.sort(null);  // OK

What are common mistakes?

WRONG: Blindly replacing collect(toList()) with .toList() in a codebase-wide refactor.

WRONG: Assuming Collectors.toList() is guaranteed to return an ArrayList. The specification says the returned list has "no guarantees on the type, mutability, serializability, or thread-safety." In practice it returns ArrayList, but you should not rely on this.

What should a senior engineer know?

  • In new code, prefer .toList() for the common read-only case. It is shorter, signals intent, and protects against accidental mutation.
  • For mutable results, use collect(Collectors.toCollection(ArrayList::new)) for an explicit, guaranteed mutable ArrayList.
  • IDE tools like IntelliJ's "Modernize" inspection may suggest replacing collect(toList()) with .toList(). Review each case to ensure the result is not mutated downstream.
  • The toList() method is defined directly on Stream as a default method, not in Collectors.

Visual Overview

DIAGRAM
  stream.collect(Collectors.toList())
       │
       ▼
  ArrayList (mutable)
  ┌────────────────┐
  │ add()    ✓ OK  │
  │ remove() ✓ OK  │
  │ sort()   ✓ OK  │
  └────────────────┘

  stream.toList()
       │
       ▼
  Unmodifiable List
  ┌────────────────────┐
  │ add()    ✗ throws!  │
  │ remove() ✗ throws!  │
  │ sort()   ✗ throws!  │
  └────────────────────┘

Key Takeaway

Stream.toList() (Java 16) and collect(Collectors.toList()) have different contracts. toList returns an unmodifiable list that throws UnsupportedOperationException on any mutation. collect(toList()) returns a mutable ArrayList that allows all modifications.

Blindly replacing collect(toList()) with .toList() during code modernization breaks any code that mutates the result. Always check if the result is modified downstream before swapping.

At National Bank of India, the account service uses .toList() for API responses (read-only, safe to share) and collect(toList()) for internal builders that need to add or sort elements.

Remember one sentence: toList() is unmodifiable and concise for final results, while collect(toList()) is mutable for results that need further modification.

---

Share
Use ← → arrow keys to navigate