Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does `List` not have a `map` default method when it has `forEach`?

Tags:

java

java-8

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?

like image 911
Thorbjørn Ravn Andersen Avatar asked Feb 21 '17 10:02

Thorbjørn Ravn Andersen


2 Answers

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.

  1. It's API bloat. The same thing can be done, in a more general way, with minor typing overhead using streams.

  2. 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).

  3. 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());
}
like image 114
Hoopje Avatar answered Oct 19 '22 23:10

Hoopje


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).

like image 43
Holger Avatar answered Oct 19 '22 23:10

Holger