Collect a Java Stream to an Immutable Collection
1. Introduction
2. Maven Dependency
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
We can get the latest version of this dependency from here.
3. Using Java’s collectingAndThen()
The collectingAndThen() method from Java’s Collectors class accepts a Collector, and a finisher Function that is applied to the result returned from the Collector:
@Test
public void whenUsingCollectingToImmutableList_thenSuccess() {
List<String> givenList = Arrays.asList("a", "b", "c");
List<String> result = givenList.stream()
.collect(collectingAndThen(toList(), ImmutableList::copyOf));
System.out.println(result.getClass());
}
Since we can’t use the toCollection() Collector directly, we need to collect elements to a temporary list and then construct an immutable list from it.
In this example, we are converting a Stream to a List using the toList() collector and then creating an ImmutableList. The ImmutableList is a part of the Guava library. If we log the output to the console, we’ll get the class of the underlying
If we log the output to the console, we’ll get the class of the underlying List implementation:
class com.google.common.collect.RegularImmutableList
4. Using Guava’s Collectors
Starting with Guava 21, with every immutable class comes an accompanying Collector that is as easy to use as standard Collectors:
@Test
public void whenCollectToImmutableList_thenSuccess() {
List<Integer> list = IntStream.range(0, 9)
.boxed()
.collect(ImmutableList.toImmutableList());
}
The resulting instance is the RegularImmutableList:
class com.google.common.collect.RegularImmutableList
5. Building a Custom Collector
Now, let’s move one step further and implement our custom Collector. To achieve this goal, we are going to 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);
}
To learn more about implementing custom Collectors, please have a look at Section 4 of this article. And that’s it. The above method is a part of our custom class
We can use it now just like any other built-in Collectors:
@Test
public void whenCollectToMyImmutableListCollector_thenSuccess() {
List<String> givenList = Arrays.asList("a", "b", "c", "d");
List<String> result = givenList.stream()
.collect(MyImmutableListCollector.toImmutableList());
}
Finally, let’s check the output:
class java.util.Collections$UnmodifiableRandomAccessList
5.1. Making the MyImmutableListCollector Generic
Our implementation has one limitation – it always returns an immutable instance backed by an ArrayList. However, with 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);
}
Now instead of determining the supplier in the method implementation, we are requesting the supplier from the user:
@Test
public void whenPassingSupplier_thenSuccess() {
List<String> givenList = Arrays.asList("a", "b", "c", "d");
List<String> result = givenList.stream()
.collect(MyImmutableListCollector.toImmutableList(LinkedList::new));
}
Please note that we are now using the LinkedList instead of ArrayList. Let’s run this and see the results:
class java.util.Collections$UnmodifiableList
This time, we got UnmodifiableList instead of UnmodifiableRandomAccessList.
6. Conclusion
Make sure to check out the full source code of this article over on GitHub.