How do I collect stream results into an immutable collection?

In Java, you can use the Stream API’s Collectors to gather stream results into an immutable collection. Since Java 10, you can use Collectors.toUnmodifiableList(), Collectors.toUnmodifiableSet(), and other similar methods to collect the results into unmodifiable collections.

Here’s how you can collect the stream results into an immutable collection:

1. Immutable List

To collect the results of a stream into an immutable list:

package org.kodejava.util.stream;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        List<String> immutableList = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableList());

        System.out.println(immutableList);

        // Attempting to modify the list will throw UnsupportedOperationException
        // immutableList.add("date"); // Throws UnsupportedOperationException
    }
}

2. Immutable Set

To collect the results into an immutable set:

package org.kodejava.util.stream;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        Set<String> immutableSet = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableSet());

        System.out.println(immutableSet);

        // Attempting to modify the set will throw UnsupportedOperationException
        // immutableSet.add("date"); // Throws UnsupportedOperationException
    }
}

3. Immutable Map

To collect results into an immutable map:

package org.kodejava.util.stream;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        Map<String, Integer> immutableMap = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableMap(
                        fruit -> fruit,         // Key mapper: the fruit itself
                        fruit -> fruit.length() // Value mapper: the length of the fruit name
                ));

        System.out.println(immutableMap);

        // Attempting to modify the map will throw UnsupportedOperationException
        // immutableMap.put("date", 4); // Throws UnsupportedOperationException
    }
}

Notes:

  • Unmodifiable vs Immutable: Collections created with Collectors.toUnmodifiableList(), Collectors.toUnmodifiableSet(), and Collectors.toUnmodifiableMap() are unmodifiable. While they cannot be changed (add, remove, replace), immutability might imply further guarantees (e.g., deeply immutable objects inside the collection, which this does not enforce).
  • Introduced in Java 10: toUnmodifiableList(), toUnmodifiableSet(), and toUnmodifiableMap() were introduced in Java 10. If you’re using Java 8 or Java 9, you’ll need a custom approach for creating immutable collections (like Collections.unmodifiableList).

In Java 8:

If you’re stuck on Java 8, you can achieve something similar using Collections.unmodifiableList() or other Collections.unmodifiableXxx methods:

package org.kodejava.util.stream;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        List<String> immutableList = Collections.unmodifiableList(
                Stream.of("apple", "banana", "cherry").collect(Collectors.toList())
        );

        System.out.println(immutableList);
        // immutableList.add("date"); // Throws UnsupportedOperationException
    }
}

This approach, however, wraps an existing modifiable collection, so try to update your project to take advantage of Java 10+ features.

How to use the Collectors.toUnmodifiableList() and other new Collectors in Java 10

In Java 10, a significant enhancement was introduced to the java.util.stream.Collectors class: new utility methods to create unmodifiable collections such as lists and sets. One notable method is Collectors.toUnmodifiableList(). This method allows you to efficiently create immutable lists during stream processing, adding to the immutability features provided by Java 9 and earlier versions.

Here’s how you can use Collectors.toUnmodifiableList() and other similar methods introduced in Java 10:


1. Using Collectors.toUnmodifiableList()

The Collectors.toUnmodifiableList() collector creates an unmodifiable list from a stream of elements. This means the resulting list cannot be modified (no adding, removing, or updating elements). If you attempt to modify it, a runtime exception (UnsupportedOperationException) will be thrown.

Example:

package org.kodejava.util.stream;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableList {
    public static void main(String[] args) {
        // Example list using Collectors.toUnmodifiableList
        List<String> unmodifiableList = Stream.of("A", "B", "C")
                .collect(Collectors.toUnmodifiableList());

        System.out.println("Unmodifiable List: " + unmodifiableList);

        // Attempt to modify the list will throw UnsupportedOperationException
        unmodifiableList.add("D"); // This will throw a runtime exception!
    }
}

Output:

Unmodifiable List: [A, B, C]
Exception in thread "main" java.lang.UnsupportedOperationException

2. Other Collectors Introduced in Java 10

Java 10 introduced two other collectors similar to toUnmodifiableList():

  • Collectors.toUnmodifiableSet()
    • Creates an unmodifiable set from a stream of elements.
    • Duplicate elements will be removed since it’s a set.

Example:

package org.kodejava.util.stream;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableSet {
    public static void main(String[] args) {
        Set<String> unmodifiableSet = Stream.of("A", "B", "C", "A") // "A" will appear only once
                .collect(Collectors.toUnmodifiableSet());
        System.out.println("Unmodifiable Set: " + unmodifiableSet);

        unmodifiableSet.add("D"); // Throws UnsupportedOperationException
    }
}
  • Collectors.toUnmodifiableMap()
    • Creates an unmodifiable map using key-value pairs from a stream.
    • Requires a way to specify the key and value in the collector.
    • If duplicate keys are produced, it will throw an IllegalStateException.

Example:

package org.kodejava.util.stream;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableMap {
    public static void main(String[] args) {
        Map<Integer, String> unmodifiableMap = Stream.of("A", "B", "C")
                .collect(Collectors.toUnmodifiableMap(
                        String::length,  // Key mapper
                        v -> v           // Value mapper
                ));

        System.out.println("Unmodifiable Map: " + unmodifiableMap);

        // Attempting to modify will throw an UnsupportedOperationException
        unmodifiableMap.put(2, "D"); // Throws UnsupportedOperationException
    }
}

3. Behavior of Unmodifiable Collections

  • These collectors guarantee that:
    • The collection cannot be modified (no add, remove, put, etc.).
    • Any attempt to modify them results in an UnsupportedOperationException.
    • They are safe to use for read-only purposes.
  • If the stream itself contains null values, a NullPointerException will be thrown.

4. Best Uses of toUnmodifiable*() Collectors

  • Ensuring immutability for collections to prevent accidental modifications.
  • Useful in multi-threaded or concurrent applications where immutability eliminates thread-safety issues.
  • Perfect for cases where only read access is required.

5. Comparison with Java 9 List.of()

Java 9 introduced factory methods like List.of(), Set.of(), and Map.of() for creating immutable collections. While those methods are concise, the new collectors offer more flexibility when working with streams.

Java 9 Example:

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

Java 10 Stream Example:

List<String> immutableList = Stream.of("A", "B", "C")
                                   .collect(Collectors.toUnmodifiableList());

Summary Table:

Collector Description Introduced in
Collectors.toUnmodifiableList() Creates an unmodifiable List Java 10
Collectors.toUnmodifiableSet() Creates an unmodifiable Set Java 10
Collectors.toUnmodifiableMap() Creates an unmodifiable Map Java 10

Conclusion

The Collectors.toUnmodifiableList() and related methods introduced in Java 10 are powerful tools for creating immutable collections directly from streams. They ensure immutability, improve code safety, and fit well into functional programming paradigms introduced with Java Streams.