The Java Streams API is lovely, but there are a few operations that One repeats over and over again which could be easier.
One example of this is Collecting to a List.
List<String> input = asList("foo", "bar"); List<String> filtered = input .stream() .filter(s -> s.startsWith("f")) .collect(Collectors.toList()); |
There are a few other annoyances too. For example I find myself using flatMap almost exclusively with methods that return a Collection not a Stream. Which means we have to do
List<String> result = input .stream() .flatMap(s -> Example.threeOf(s).stream()) .collect(Collectors.toList()); //... public static List<String> threeOf(String input) { return asList(input, input, input); } |
It would be nice to have a convenience method to allow us to use a method reference in this very common scenario
List<String> result = input .stream() .flatMap(Example::threeOf) // Won't compile, threeOf returns a collection, not a stream .collect(Collectors.toList()); |
Benjamin Winterberg has written a good guide to getting intellij to generate these for us . But we could extend the API ourselves.
Extending the Streams API is a little tricky as it is chainable, but it is possible. We can use the Forwarding Interface Pattern to add methods to List, but we want to add a new method to Stream.
What we can end up with is
EnhancedList<String> input = () -> asList("foo","bar"); List<String> filtered = list .stream() .filter(s -> s.startsWith("f")) .toList(); |
To do this we can first use the Forwarding Interface Pattern to create an enhanced Stream with our toList method. Starting with a ForwardingStrem
interface ForwardingStream<T> extends Stream<T> { Stream<T> delegate(); default Stream<T> filter(Predicate<? super T> predicate) { return delegate().filter(predicate); } // Other methods omitted for brevity } |
Now, since Stream provides a chainable api, with methods that return Streams – when we implement our EnhancedStream we need to change the subset of methods that return a Stream to return our EnhancedStream.
interface EnhancedStream<T> extends ForwardingStream<T> { default EnhancedStream<T> filter(Predicate<? super T> predicate) { return () -> ForwardingStream.super.filter(predicate); } // Other methods omitted for brevity } |
Note that the filter method already exists on Stream and ForwardingStream, but as Java supports covariant return types we can override it and change the returntype to a more specific type (In this case from Stream to EnhancedStream).
You may also notice the lambda returned from the filter() method. Since ForwardingStream is a single method interface with just a Stream
Now we can add our additional behaviour to the EnhancedStream. Let’s add the toList() method, and a new flatMapCollection
interface EnhancedStream<T> extends ForwardingStream<T> { default List<T> toList() { return collect(Collectors.toList()); } default <R> EnhancedStream<R> flatMapCollection(Function<? super T, ? extends Collection<? extends R>> mapper) { return flatMap(mapper.andThen(Collection::stream)); } // Other methods omitted for brevity } |
Finally, we’ll need to hook up the Stream to our List. We can use the same technique of a forwarding interface which overrides a method and specifies a more specific return type.
interface EnhancedList<T> extends ForwardingList<T> { default EnhancedStream<T> stream() { return () -> ForwardingList.super.stream(); } } interface ForwardingList<T> extends List<T> { List<T> delegate(); default int size() { return delegate().size(); } // All the other methods omitted for brevity } |
So now we can put it all together. EnhancedList is a single method interface so compatible with any existing List in our code, we just wrap it in a lambda, and then we can flatMapCollection() and toList(). The following prints “foo” three times.
EnhancedList<String> input = () -> asList("foo", "bar"); List<String> result = input .stream() .flatMapCollection(Example::threeOf) .toList(); result.forEach(System.out::println); // ... public static List<String> threeOf(String input) { return asList(input, input, input); } |