benjiPosted under Java.

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 delegate() method it is compatible with a lambda that supplies a delegate Stream. Equivalent to a Supplier. EnhancedStream extends ForwardingStream and doesn’t declare any additional abstract methods, so we can return lambdas from each method that needs to return a Stream, delegating to the ForwardingStream. The ForwardingStream.super.filter() syntax allows us to call the implementation already defined in ForwardingStream explicitly.

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);
}

Here’s the unabbreviated code.

Leave a Reply

  • (will not be published)