Stream.toList() and other converter methods I’ve wanted since Java 2
Better late than never?
Self-service only
In 2004, I was an architect coding in Java at a large financial services firm. Java was missing most of the collection productivity features I had in Smalltalk, so I decided I would “Just do it” and started building the first utility classes in what would eventually become an open-source Java library called Eclipse Collections. I blogged about this a year ago.
Serving a full menu dinner every day for 40 years
Smalltalk has always had converter methods for its collection types. A converter method allows you to convert one type to another via an intention revealing method name. In Smalltalk, the converter methods all started with the prefix “as”. I created a mind map capturing the converter methods available on the Smalltalk Collection API. Smalltalk has had these methods for the past 40 years.
These methods have been proven useful for the past 40 years by Smalltalk developers. I used all of them at one time or other when I was developing professionally in Smalltalk in the 1990's.
Smalltalk -> Java equivalents
asArray
->toArray
asDictionary
->Collectors.toMap
asOrderedCollection
->Collectors.toList
asSet
->Collectors.toSet
Java uses the prefix “to” for converter methods in the Collectors
class and for toArray
on Collection
and Stream
. The other methods available in Smalltalk have no equivalent in Collectors
today.
So why did Smalltalk add the symmetric converter methods for List
, Bag
,Set
, Map
, IdentitySet
, OrderedMap
, SortedList
40 years ago? I believe it’s because the benefit far outweighs the cost of each of the methods.
Planning the perfect dinner for 7 years
When Java 16 is released in March, seven years will have passed since Java 8 was released with lambdas and Streams. The new Stream.toList()
method in Java 16 will be somewhat disappointing without its family and friends (toSet
, toMap
, toCollection
).
Doing a quick search on GitHub, I found the following occurrences of Collectors.toList()
, Collectors.toSet()
, Collectors.toMap()
, and Collectors.toCollection()
.
Collectors.toList()
->1,363,648
Collectors.toSet()
->283,944
Collectors.toMap()
->169,753
Collectors.toCollection()
->61,831
Collectors.counting()
->14,765
(equivalent oftoBag
)Collectors.toUnmodifiableList()
->4,712
Collectors.toUnmodifiableSet()
->2,064
Collectors.toUnmodifiableMap()
->1,386
Clearly, toList()
is the most commonly used method. This is not surprising. But there are over half a million other instances of converter method usages. This is only searching for projects in GitHub.
Most enterprise applications are not in GitHub. I would expect at least one to two orders of magnitude more usages for each of these if we could scan all private Java code repos in the world.
After toList
is added to Stream in JDK 16, how many months or years will it be before we see toSet
, toMap
and toCollection
?
Hobson’s Dinner Menu
When we get Stream.toList()
in Java 16, it will be a choice of one meal for dinner every night. We can use any converter method we want on Stream
as long as it is toList
. This is a Hobson’s choice.
If we need another converter method, we can use collect
and Collectors
for other collection types. This will lead to an unfortunate asymmetry in the Stream
API usage. The method toList
will be very fluent, whereas using Collectors
for other types is less so.
Mutable or Immutable or Unmodifiable
Naming is important. Naming something well is difficult. List
is a mutable interface. There are “unmodifiable” implementations of the interface, which makes the List
interface “conditionally” mutable. This is similar to the “synchronized” implementations of List
which provide “conditional thread safety”.
With anything that is conditional, it is up to the developer to understand and handle any potential conditionality. I would expect a method named toList
, to return a mutable type.
The name does not indicate that an unmodifiable or immutable implementation is returned. The caller has to look past the name and the return type, and look at the specification in Javadoc or explore the implementation code directly to understand the behavior supported by the result returned.
Since an unmodifiable implementation is returned, the method would have been more clear if it was named toUnmodifiableList
. This name would have provided good symmetry with Collectors.toUnmodifiableList()
. Stream.toList
is actually more similar to Collectors.toUnmodifiabList
. If toList
actually returned a mutable List
, it would have provided good symmetry with Collectors.toList()
.
Stream.toCollection = All you can eat buffet
It would be very beneficial to add the method toCollection
to Stream
. Collectors.toCollection
takes a Supplier
that returns a subtype of Collection
. The actual implementation type returned is up to the developer, and all implementations must be mutable.
<T, R extends Collection<T>> R toCollection(Supplier<R> supplier)
This allows Stream
working nicely with all types of Collection
in the universe. It would also be consistent with the same method on Collectors
. Its implementation can be covered simply via a default method. If brevity was desired, I would shorten the name to simply “to”, since the Supplier
clarifies the return type already.
<T, R extends Collection<T>> R to(Supplier<R> supplier)
Multiple Choice Dinner Menu
If the symmetry of converter methods is important to you, you do have a choice. You can always use Eclipse Collections which has many converter methods on both object and primitive Iterables. The converter methods available on RichIterable
are as follows:
RichIterable
is the parent interface of most of the object type containers in Eclipse Collections.
If you would like to learn about the many converter methods that are available in Eclipse Collections today, there is a blog I wrote at the end of 2020, with a related code kata.
Summary
The method Stream.toList()
which will arrive in JDK 16 with an unmodifiable return type will probably cause some confusion. I teach Java to both new and experienced developers.
The name toList
is not intention revealing by itself. Naming the method toUnmodifiableList
would have made it consistent with the same method on Collectors
.
Serving well-done hamburgers with only one bun after a 7-year wait, when it says medium rare hamburgers on the menu may leave some developers unsatisfied. Having no fish or vegetarian meal option may also be disappointing (toSet
and toMap
).
In JDK 17, adding the method toCollection
to Stream
would cover many more use cases than the new Stream.toList
method. There will be no confusion in the naming, as it will be completely consistent with the method named toCollection
on Collectors
.
This would give developers a nearly unlimited menu. Since we’re getting toList
it anyway, we really should add this method to keep developer’s appetites satisfied.
Eclipse Collections already has many converter methods for object, primitive, serial, parallel, eager, lazy, mutable, and immutable APIs. Having Stream.toCollection
would allow fluent conversions to the extensive number of collection types to convert to in Eclipse Collections from Streams.
This would be an excellent addition. I plan to start advocating for it to be included in JDK 17. I would prefer the simpler and shorter name of to
, but would settle for toCollection
to be consistent with Collectors.toCollection
. Consistency and clarity are more important than brevity.
I am a Project Lead and Committer for the Eclipse Collections OSS project at the Eclipse Foundation. Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.