Collect a Java Stream to an Immutable Collection

1. Introduction

We often wish to convert a Java Stream into a collection. This usually results in a mutable collection, but we can customize it.

In this short tutorial, we’re going to take a close look at how to collect a Java Stream to an immutable collection – first using plain Java, and then using the Guava library.

2. Using Standard Java

2.1. Using Java’s toUnmodifiableList

Starting with Java 10, we can use the toUnmodifiableList method from Java’s Collectors class:

List<String> givenList = Arrays.asList("a", "b", "c");
List<String> result = givenList.stream()
  .collect(toUnmodifiableList());

By using this method, we get a List implementation that doesn’t support null values from Java’s ImmutableCollections:

class java.util.ImmutableCollections$ListN

2.2. Using Java’s collectingAndThen

The collectingAndThen method from Java’s Collectors class accepts a Collector and a finisher Function. This finisher is applied to the result returned from the Collector:

List<String> givenList = Arrays.asList("a", "b", "c");
List<String> result = givenList.stream()
  .collect(collectingAndThen(toList(), ImmutableList::copyOf));

System.out.println(result.getClass());

With this approach, since we can’t use the toCollection Collector directly, we need to collect elements into a temporary list. Then, we construct an immutable list from it.

2.3. Using Stream.toList() method

Java 16 introduces a new method on Stream API called toList(). This handy method returns an unmodifiable List containing the stream elements:

@Test
public void whenUsingStreamToList_thenReturnImmutableList() {
    List<String> immutableList = Stream.of("a", "b", "c", "d").toList();
	
    Assertions.assertThrows(UnsupportedOperationException.class, () -> {
        immutableList.add("e");
    });
}

As we can see in the unit test, Stream.toList() returns an immutable listSo, trying to add a new element to the list will simply lead to UnsupportedOperationException.

Please bear in mind that the new Stream.toList() method is slightly different from the existing Collectors.toList() as it returns an unmodifiable list.

3. Building a Custom Collector

We also have the option to implement a custom Collector.

3.1. A Basic Immutable Collector

To achieve this, we can use the static Collector.of method:

public static <T> Collector<T, List<T>, List<T>> toImmutableList() {
    return Collector.of(ArrayList::new, List::add,
      (left, right) -> {
        left.addAll(right);
        return left;
      }, Collections::unmodifiableList);
}

We can use this function just like any built-in Collector:

List<String> givenList = Arrays.asList("a", "b", "c", "d");
List<String> result = givenList.stream()
  .collect(MyImmutableListCollector.toImmutableList());

Finally, let’s check the output type:

class java.util.Collections$UnmodifiableRandomAccessList

3.2. Making the MyImmutableListCollector Generic

Our implementation has one limitation – it always returns an immutable instance backed by an ArrayList. However, with a slight improvement, we can make this collector return a user-specified type:

public static <T, A extends List<T>> Collector<T, A, List<T>> toImmutableList(
  Supplier<A> supplier) {
 
    return Collector.of(
      supplier,
      List::add, (left, right) -> {
        left.addAll(right);
        return left;
      }, Collections::unmodifiableList);
}

So now, instead of determining the Supplier in the method implementation, we’re requesting the Supplier from the user:

List<String> givenList = Arrays.asList("a", "b", "c", "d");
List<String> result = givenList.stream()
  .collect(MyImmutableListCollector.toImmutableList(LinkedList::new));

Also, we’re using the LinkedList instead of ArrayList.

class java.util.Collections$UnmodifiableList

This time, we got UnmodifiableList instead of UnmodifiableRandomAccessList.

4. Using Guava’s Collectors

In this section, we’re going to use the Google Guava library to drive some of our examples:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>22.0</version>
</dependency>

Starting with Guava 21, every immutable class comes with an accompanying Collector that’s as easy to use as Java’s standard Collectors:

List<Integer> list = IntStream.range(0, 9)
  .boxed()
  .collect(ImmutableList.toImmutableList());

The resulting instance is the RegularImmutableList:

class com.google.common.collect.RegularImmutableList

5. Conclusion

In this short article, we’ve seen various ways to collect a Stream into an immutable Collection.

As always, the full source code of this article is over on GitHub. They’re separated by Java version into examples for sections 3-4, section 2.2, and section 2.3.

Related posts:

Rest Web service: Filter và Interceptor với Jersey 2.x (P2)
Java Program to Implement First Fit Decreasing for 1-D Objects and M Bins
Refactoring Design Pattern với tính năng mới trong Java 8
RestTemplate Post Request with JSON
Java Program to Implement ArrayBlockingQueue API
Java Program to Use Above Below Primitive to Test Whether Two Lines Intersect
How to Get All Dates Between Two Dates?
Java Program to Implement Sparse Array
Create Java Applet to Simulate Any Sorting Technique
TreeSet và sử dụng Comparable, Comparator trong java
Java Program to Generate All Subsets of a Given Set in the Lexico Graphic Order
How to Change the Default Port in Spring Boot
Java Program to Implement ConcurrentHashMap API
How to Define a Spring Boot Filter?
Handling Errors in Spring WebFlux
The Order of Tests in JUnit
Java Program to Convert a Decimal Number to Binary Number using Stacks
Java – Try with Resources
Java Program to Implement Queue using Linked List
Optional trong Java 8
How to Count Duplicate Elements in Arraylist
Spring Boot With H2 Database
Giới thiệu Java Service Provider Interface (SPI) – Tạo các ứng dụng Java dễ mở rộng
Java Program to Implement Rope
String Operations with Java Streams
Introduction to Spring Cloud Netflix – Eureka
Java Program to Implement Max Heap
Java Map With Case-Insensitive Keys
Java Program to Generate All Pairs of Subsets Whose Union Make the Set
Guide to the Fork/Join Framework in Java
Extra Login Fields with Spring Security
Quick Guide to Spring Bean Scopes