Monday, December 7, 2020

JDK 16: Stream to List In One Easy Call

As Java functional streams have become increasingly popular, an increasing number of requests is being made for new stream operations to be supported. Amidst these requests for numerous disparate new operations, one operation that seems to be requested more than the others is an operation that directly provides a List from a Stream. JDK 16 Early Access Build 27 introduces Stream.toList(), which is the subject of this post.

Before the JDK 16 Early Access Build 27 introduction of Stream.toList(), the most common approach for acquiring a List from a Stream was to invoke the approprite Collector:

stream.collect(Collectors.toList())

This is not a lot of code and it's fairly straightforward once you see it, but many have wanted an even more concise syntax for this frequently used stream operation. JDK 16 brings us this:

stream.toList()

It may be tempting to go into one's code base and use stream.toList() as a drop-in replacement for stream.collect(Collectors.toList()), but there may be differences in behavior if the code has a direct or indirect dependency on the implementation of stream.collect(Collectors.toList()) returning an ArrayList. Some of the key differences between the List returned by stream.collect(Collectors.toList()) and stream.toList() are spelled out in the remainder of this post.

The Javadoc-based documentation for Collectors.toList() states (emphasis added), "Returns a Collector that accumulates the input elements into a new List. There are no guarantees on the type, mutability, serializability, or thread-safety of the List returned..." Although there are no guarantees regarding the "type, mutability, serializability, or thread-safety" on the List provided by Collectors.toList(), it is expected that some may have realized it's currently an ArrayList and have used it in ways that depend on the characteristics of an ArrayList.

The following code snippet (full code listing on GitHub) shows a method that can be executed against the List implementations returned by Collectors.toList() and Stream.toList() to see what they have in common and how the are different.

/**
 * Analyzes the supplied {@code List} and writes to standard output
 * some key characteristics of the supplied {@code List}.
 *
 * @param listDescription Description of {@code List} to be analyzed.
 * @param listUnderAnalysis {@code List} to be analyzed.
 */
private static void analyzeList(
   final String listDescription, final List<String> listUnderAnalysis)
{
   out.println(listDescription + ": ");
   out.println("\tClass Type: " + listUnderAnalysis.getClass().getCanonicalName());
   out.println("\tAble to add to List? " + isListAddCapable(listUnderAnalysis));
   out.println("\tAble to sort List?   " + isListSortable(listUnderAnalysis));
}

When the simple analysis code above is executed against implementations of List returned by Stream.collect(Collectors.toList()) and Stream.toList(), the output appears as shown next.

Stream.collect(Collectors.toList()): 
	Class Type: java.util.ArrayList
	Able to add to List? true
	Able to sort List?   true
Stream.toList(): 
	Class Type: java.util.ImmutableCollections.ListN
	Able to add to List? false
	Able to sort List?   false
[NOT Stream] List.of(): 
	Class Type: java.util.ImmutableCollections.ListN
	Able to add to List? false
	Able to sort List?   false

The output shown above demonstrates that Stream.toList() provides a List implementation that is immutable (type ImmutableCollections.ListN that cannot be added to or sorted) similar to that provided by List.of() and in contrast to the mutable (can be changed and sorted) ArrayList provided by Stream.collect(Collectors.toList()). Any existing code depending on the ability to mutate the ArrayList returned by Stream.collect(Collectors.toList()) will not work with Stream.toList() and an UnsupportedOperationException will be thrown.

Although the implementation nature of the Lists returned by Stream.collect(Collectors.toList()) and Stream.toList() are very different, they still both implement the List interface and so they are considered equal when compared using List.equals(Object). This is demonstrated in the full code listing on GitHub.

The addition of method toList() to the Stream interface is a small thing, but it does make an often-used technique more convenient.

2 comments:

Manoj Krishna said...

Nice one !!

Unknown said...

Small feature but very useful 👍