43

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?

19
  • 17
    I would ask the opposite question: why toArray() instead of collect(toArray()). API explosion is something the JDK tends to fend off as much as possible. I expect there to be good justification for toArray(). Commented Feb 28, 2015 at 13:25
  • 10
    Why stop on toList? Lets also add toStack toSet toMap.
    – Pshemo
    Commented Feb 28, 2015 at 13:31
  • 12
  • 7
    with a static import you can reduce it to collect(toList())
    – assylias
    Commented Feb 28, 2015 at 14:54
  • 27
    @MarkoTopolnik Your observation about the slippery slope did indeed factor into the decision. The reason arrays are special is: they are special. 1) They're built into the language (and therefore have more claim for inclusion), and 2) excluding them would have given up on some big parallelism opportunities (our implementation of toArray exploits characteristics like SUBSIZED; in the best case, toArray allocates one big array and writes the elements concurrently into place, with zero copying or locking.) Commented Feb 28, 2015 at 20:00

4 Answers 4

14

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.

13

Java 16 introduced Stream.toList():

Stream.of(-2,1,2,-5)
    .filter(n -> n > 0)
    .map(n -> n * n)
    .toList();

The new method is slightly different from the existing collect(toList()): it returns an unmodifiable list.

6

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

0
1

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));
1
  • 3
    If you're going to do this you might as well go with Collectors.toList() just for the sake of comprehensibility. Commented Sep 6, 2018 at 7:59

Not the answer you're looking for? Browse other questions tagged or ask your own question.