40

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?

ZhekaKozlov
  • 36,558
  • 20
  • 126
  • 155
Jiri Kremser
  • 12,471
  • 7
  • 45
  • 72
  • 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()`. – Marko Topolnik Feb 28 '15 at 13:25
  • 10
    Why stop on `toList`? Lets also add `toStack` `toSet` `toMap`. – Pshemo Feb 28 '15 at 13:31
  • @MarkoTopolnik right, thx, I eddited the answer – Jiri Kremser Feb 28 '15 at 13:36
  • 4
    ...but I don't think this should be closed as "Primarily opinion-based". I'm sure the Java language architects active on this site can give an authoritative answer. In fact, I suspect they have already done so and my Google-foo is letting me down. – Marko Topolnik Feb 28 '15 at 13:36
  • 2
    @Pshemo I don't have any numbers, but in my opinion the list is the far most used collection out there. The reason is the convenience (and Scala has it :]) – Jiri Kremser Feb 28 '15 at 13:38
  • 2
    The relevant number here is that of those who would, in the alternate reality where `toList()` existed, complain about the lack of `toSet()`, `toMap()`, and all other `toX()`s combined. I suspect that the number is very much the same, and this way we have one less method to scroll down through while vading through the vast domains of the `Stream` API. – Marko Topolnik Feb 28 '15 at 13:44
  • 12
    Here: http://mail.openjdk.java.net/pipermail/lambda-dev/2013-May/009798.html – Marko Topolnik Feb 28 '15 at 13:50
  • 7
    with a static import you can reduce it to `collect(toList())` – assylias Feb 28 '15 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.) – Brian Goetz Feb 28 '15 at 20:00
  • 3
    @BrianGoetz Yes, the key difference between `collect(toArray())` and `toArray()` is in the amount of coupling. BTW this would suggest that `asList(stream.toArray())` is something to consider instead of `stream.collect(toList())` when parallel performance is critical. – Marko Topolnik Mar 01 '15 at 09:16
  • @MarkoTopolnik Looks like `asList(stream.toArray())` is slower than `stream.collect(toList())`, at least on JDK 1.8.0.20. Check [this thread](http://stackoverflow.com/questions/28319064/java-8-best-way-to-transform-a-list-map-or-foreach) for some benchmarks – harshtuna Mar 12 '15 at 02:19
  • 6
    @harshtuna You haven't done enough warmup. Also, don't include warmup in your timing. I've benchmarked this using JMH and without doing any extra work with filter and map (that just dilutes the differences). With the size of 2 million I see 8.5 ns per element for `toArray()` and 20.3 ns for `collect(toList())` in the sequential case. For parallel I see modest speedup for `toArray()` and a *slowdown* for `collect(toList())` (that's because I don't do any other work). – Marko Topolnik Mar 12 '15 at 06:39
  • 1
    @harshtuna: that’s a different case than Brian Goetz described as a filtered stream has *no* `SIZED` characteristics. Therefore, `toArray` has to predict and merge similar to `collect` in this case, hence does not exploit the big advantage that it has for streams with a predictable size. – Holger Mar 12 '15 at 17:18
  • @Holger, fair point, Splitterator is still a bit mystery for me – harshtuna Mar 12 '15 at 20:51
  • What I miss much more is a `toIterable()`. With it I could feed anything I'd want, plus could just iterate over it. (Ok, I can do `(Iterable)(stream::iterator)`, but that's ugly as well.) Or a `Stream` could just implement `Iterable`, then all would be fine... – glglgl Mar 24 '15 at 17:42
  • @BrianGoetz "excluding them would have given up on some big parallelism opportunities" Not if it would have been implemented under `Collectors` as well. – glglgl Mar 25 '15 at 07:35
  • 3
    @glglgl No. Try implementing it; you'll find the Collector API insufficient to pull this off with no copying. It would have complicated the Collector API substantially to track sufficient information to do this as efficiently as is done inside of Stream. – Brian Goetz Mar 25 '15 at 13:28
  • 1
    Arrays.asList is fairly effective. – yesennes Apr 26 '15 at 04:24
  • Cause java does not care about developers and lambda is just akward to work with if compared to c#. Not just toList but also the whole structure is off. C# lambdas follow always simple SQL syntax in defined order `FROM, WHERE, GROUP, SELECT / PROJECTION`. Java lambda it depends on what you want to do. Instead of `stream.filter(...).any()` we do `stream.any(...)`. It saves some minor typing but makes the stream / query not reusable. Sometimes you want first to check any and then do some other stuff with same filter. I find java lambda as a big of missconcept as old java Date namespace. – djmj Mar 22 '21 at 02:02

4 Answers4

13

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.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
11

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.

Tordanik
  • 1,188
  • 11
  • 29
ZhekaKozlov
  • 36,558
  • 20
  • 126
  • 155
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.

fps
  • 33,623
  • 8
  • 55
  • 110
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));
Mike Twain
  • 66
  • 3