When using the Java 8 streams, it's quite common to take a list, create a stream from it, do the business and convert it back. Something like:
Stream.of(-2,1,2,-5)
.filter(n -> n > 0)
.map(n -> n * n)
.collect(Collectors.toList());
Why there is no short-cut/convenient method for the '.collect(Collectors.toList())
' part? On Stream interface, there is method for converting the results to array called toArray()
, why the toList()
is missing?
IMHO, converting the result to list is more common than to array. I can live with that, but it is quite annoying to call this ugliness.
Any ideas?
Recently I wrote a small library called StreamEx which extends Java streams and provides this exact method among many other features:
StreamEx.of(-2,1,2,-5)
.filter(n -> n > 0)
.map(n -> n * n)
.toList();
Also toSet(), toCollection(Supplier), joining(), groupingBy() and other shortcut methods are available there.
As for the "why", I believe there are quite a lot of arguments in the comments. However, I agree with you in that it's quite annoying to not have a toList()
method. Same happens with a toIterable()
method.
So I'll show you a trick that lets you use these two methods anyway. Fortunately, Java is very flexible and allows you to do all kinds of interesting stuff. About 10 years ago, I read this article, which describes a witty trick to "plug" methods to any given interface. The trick consists of using a proxy to adapt the interface that doesn't have the methods you want. Over the years, I've found that it has all adapter pattern's pros, whereas it lacks all of its cons. That's what I call a big deal.
Here's a sample code, just to show the idea:
public class Streams {
public interface EnhancedStream<T>
extends Stream<T> {
List<T> toList();
Iterable<T> toIterable();
}
@SuppressWarnings("unchecked")
public static <T> EnhancedStream<T> enhance(Stream<T> stream) {
return (EnhancedStream<T>) Proxy.newProxyInstance(
EnhancedStream.class.getClassLoader(),
new Class<?>[] {EnhancedStream.class},
(proxy, method, args) -> {
if ("toList".equals(method.getName())) {
return stream.collect(Collectors.toList());
} else if ("toIterable".equals(method.getName())) {
return (Iterable<T>) stream::iterator;
} else {
// invoke method on the actual stream
return method.invoke(stream, args);
}
});
}
public static void main(String[] args) {
Stream<Integer> stream1 = Stream.of(-2, 1, 2, -5).
filter(n -> n > 0).map(n -> n * n);
List<Integer> list = Streams.enhance(stream1).toList();
System.out.println(list); // [1, 4]
Stream<Integer> stream2 = Stream.of(-2, 1, 2, -5).
filter(n -> n > 0).map(n -> n * n);
Iterable<Integer> iterable = Streams.enhance(stream2).toIterable();
iterable.forEach(System.out::println); // 1
// 4
}
}
The idea is to use an EnhancedStream
interface that extends Java's Stream
interface by defining the methods you want to add. Then, a dynamic proxy implements this extended interface by delegating original Stream
methods to the actual stream being adapted, while it just provides an inline implementation to the new methods (the ones not defined in Stream
).
This proxy is available by means of a static method that transparently performs all proxying stuff.
Please note that I'm not stating that this is a final solution. Instead, it's just an example that can be highly improved, i.e. for every method of Stream
that returns another Stream
, you could return a proxy for that one too. This would allow EnhancedStream
s to be chained (you'd need to redefine these methods in the EnhancedStream
interface, so that they return an EnhancedStream
covariant return type). Besides, proper exception handling is missing, as well as more robust code to decide whether to delegate the execution of methods to the original stream or not.
Here's a simple helper class that makes this easier:
public class Li {
public static <T> List<T> st(final Stream<T> stream) {
return stream.collect(Collectors.toList());
}
}
Example use:
List<String> lowered = Li.st(Stream.of("HELLO", "WORLD").map(String::toLowerCase));