I have looked into writing streams-based code in Java 8, and have noticed a pattern, namely that I frequently have a list but have the need to transform it to another list by applying a trivial mapping to each element. After writing .stream().map(...).collect(Collections.toList())
yet another time I remembered we have List.forEach
so I looked for List.map
but apparently this default method has not been added.
Why was List.map()
(EDIT: or List.transform()
or List.mumble()
)
not added (this is a history question) and is there a simple shorthand using other methods in the default runtime library that does the same thing that I have just not noticed?
Of course, I can't look into the head of the Java designers, but I can think of a number of reasons not to include a map
(or other stream methods) on collections.
It's API bloat. The same thing can be done, in a more general way, with minor typing overhead using streams.
It leads to code bloat. If I called map
on a list, I would expect the result to have the same runtime type (or at least with the runtime properties) as the list I called it on. So for a ArrayList.map
would return an ArrayList
, LinkedList.map
a LinkedList
etc. That means that the same functionality would need to be implemented in all List
implementations (with a suitable default implementation in the interface so old code will not be broken).
It would encourage code like list.map(function1).map(function2)
, which is considerably less efficient than list.stream().map(function1).map(function2).collect(Collectors.toList())
because the former constructs an auxiliary list which is immediately thrown away, while the latter applies both functions to the list elements and only then constructs the result list.
For a functional language like Scala the balance between advantages and disadvantages might be different.
I do not know of shortcuts in the Java standard library, but you can of course implement your own:
public static <S,T> List<T> mapList(List<S> list, Function<S,T> function) {
return list.stream().map(function).collect(Collectors.toList());
}
As explained in “Why doesn't java.util.Collection implement the new Stream interface?” the design decision to separate the Collection
API and the Stream
API was made to separate eager and lazy operations.
In this regard, several bulk operation were added to the Collection API:
List.replaceAll(UnaryOperator)
List.sort(Comparator)
Map.replaceAll(BiFunction)
Collection.removeIf(Predicate)
Map.forEach(BiConsumer)
Iterable.forEach(Consumer)
Common to all these eager methods is that functions which evaluate to a result are used to modify the underlying Collection. A map
method returning a new Iterable
or Collection
wouldn’t fit into the scheme.
Further, among these methods, forEach(Consumer)
is the only one that happens to have a signature matching a Stream
method. Which is unfortunate, as these methods don’t even do the same; the closest equivalent to Iterable.forEach(Consumer)
is Stream.forEachOrdered(Consumer)
. But it is also clear, why there is a functional overlap.
Performing an action for its side effect for each element is the only bulk operation that doesn’t modify the source collection, hence can be offered by the Stream API as well (as a terminal operation). There, it would be chained after one or more lazily evaluated intermediate operations; using it without prepended intermediate operations, is a special case.
Since map
isn’t a terminal operation, it wouldn’t fit into the scheme of Collection methods at all. The closest equivalent is List.replaceAll(UnaryOperator)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With