Differences of Java 16's Stream.toList() and Stream.collect(Collectors.toList())?
Asked Answered
S

4

101

JDK 16 now includes a toList() method directly on Stream instances. In previous Java versions, you always had to use the collect method and provide a Collector instance.

The new method is obviously fewer characters to type. Are both methods interchangeable or are there subtle differences one should be aware of?

var newList = someCollection.stream()
    .map(x -> mapX(x))
    .filter(x -> filterX(x))
    .toList();

// vs.

var oldList = someCollection.stream()
    .map(x -> mapX(x))
    .filter(x -> filterX(x))
    .collect(Collectors.toList());

(This question is similar to Would Stream.toList() perform better than Collectors.toList(), but focused on behavior and not (only) on performance.)

Soul answered 30/1, 2021 at 15:44 Comment(0)
G
94

One difference is that Stream.toList() provides a List implementation that is immutable (type ImmutableCollections.ListN that cannot be added to or sorted) similar to that provided by List.of() and in contrast to the mutable (can be changed and sorted) ArrayList provided by Stream.collect(Collectors.toList()).

Demo:

import java.util.stream.Stream;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = Stream.of("Hello").toList();
        System.out.println(list);
        list.add("Hi");
    }
}

Output:

[Hello]
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
    at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
    at Main.main(Main.java:8)

Please check this article for more details.

Update:

Interestingly, Stream.toList() returns a nulls-containing list successfully.

import java.util.stream.Stream;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Object> list = Stream.of(null, null).toList();
        System.out.println(list);
    }
}

Output:

[null, null]

On the other hand, List.of(null, null) throws NullPointerException.

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Object> list = List.of(null, null);
    }
}

Output:

Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:208)
    at java.base/java.util.ImmutableCollections$List12.<init>(ImmutableCollections.java:453)
    at java.base/java.util.List.of(List.java:827)
    at Main.main(Main.java:5)

Note: I've used openjdk-16-ea+34_osx-x64 to compile and execute the Java SE 16 code.

Useful resources:

  1. JDK Bug#JDK-8180352
  2. Calling Java varargs method with single null argument?
Greta answered 30/1, 2021 at 16:4 Comment(10)
Does this subsequently mean that .toList() can never return a null-containing list, but .collect(toList() can? i.e. Stream.of(null, null).toList() throws, Stream.of(null, null).collect(toList()) returns?Soul
IIRC, Collectors.toList() is not guaranteed to give us a mutable list either. It just happens to do so in the Java versions we have seen so far.Bowknot
@OleV.V. - Correct. The article linked in my answer mentions it as: Although there are no guarantees regarding the “type, mutability, serializability, or thread-safety” on the List provided by Collectors.toList(), it is expected that some may have realized it’s currently an ArrayList and have used it in ways that depend on the characteristics of an ArrayList.Greta
it's good that you edited, I have no idea how my comment was removed though :|Ascot
Nothing directed to the author, and maybe something for a new question, but isn't it strange that it returns a List, but you can't use half of the methods on that interface? Doesn't really implement that interface then, does it?Sigmatism
@Sigmatism The List interface was on purpose designed with a number of optional methods, that is, methods that implementing classes need not implement and that many implementations do not implement. You’re not the first to wonder.Bowknot
It is not really a correct statement to say that collect(toList()) returns a mutable list; the specification is very clear that it makes no guarantees as to the mutability of the returned list. The current implementation happens to return an ArrayList right now, but the spec was written explicitly to allow that to change. If you want a mutable list, use toCollection(ArrayList::new).Earlearla
It is unfortunate that List was not divided into ReadableList with get() etc. and WritableList with set(), add() etc., but it's too late to fix now. (note: I originally came here to suggest toCollection(ArrayList::new), which is what I always use when I need a mutable list, but The Man Himself beat me to it. 😂)Kep
The new method Stream.toList() produces neither an unmodifiable list nor a shortcut to collect(toUnmodifiableList()), because toUnmodifiableList() doesn’t accept nulls.The implementation of Stream.toList() is not constrained by the Collector interface; therefore, Stream.toList() allocates less memory. blogs.oracle.com/javamagazine/hidden-gems-jdk16-jdk17-jep Can you Validate the above statements in the article ?Jovitta
@ClementCherlin For advanced list APIs, please check Eclipse Collections - a much underrated Java data structure library.Thanatopsis
A
61

Here is a small table that summarizes the differences between Stream.collect(Collectors.toList()), Stream.collect(Collectors.toUnmodifiableList()) and Stream.toList():

Method Guarantees unmodifiability Allows nulls Since
collect(toList()) No Yes Java 8
collect(toUnmodifiableList()) Yes No Java 10
toList() Yes Yes Java 16

Another small difference:

// Compiles
List<CharSequence> list = Stream.of("hello", "world").collect(toList());

// Error
List<CharSequence> list = Stream.of("hello", "world").toList();
Aundrea answered 1/2, 2021 at 11:27 Comment(3)
The new method Stream.toList() produces neither an unmodifiable list nor a shortcut to collect(toUnmodifiableList()), because toUnmodifiableList() doesn’t accept nulls.The implementation of Stream.toList() is not constrained by the Collector interface; therefore, Stream.toList() allocates less memory. That makes it optimal to use when the stream size is known in advance. blogs.oracle.com/javamagazine/hidden-gems-jdk16-jdk17-jep Can you Validate the above statementsJovitta
I would add that the first one was introduced in Java 8, the 2nd in Java 10, and the 3rd in Java 16.Ballyhoo
@Jovitta on the contrary, Stream.toList() guarantees an unmodifiable list. It is indeed not a shortcut to Collectors.toUnmodifiableList()).Kep
D
17

.collect(toList()) and toList() behave different regarding sub type compatibility of the elements in the created lists.

Have a look at the following alternatives:

  • List<Number> old = Stream.of(0).collect(Collectors.toList()); works fine, although we collect a stream of Integer into a list of Number.
  • List<Number> new = Stream.of(0).toList(); is the equivalent Java 16+ version, but it doesn't compile (cannot convert from List<Integer> to List<Number>; at least in ecj, the Eclipse Java compiler).

There are at least 2 workarounds to fix the compile error:

  • Explicitly cast to the wanted type List<Number> fix1 = Stream.of(0).map(Number.class::cast).toList();
  • allow sub types in the result collection: List<? extends Number> fix2 = Stream.of(0).toList();

To my understanding the root cause is as follows: The generic type T of the Java 16 toList() is the same as the generic type T of the Stream itself. The generic type T of Collectors.toList() however is propagated from the left hand side of the assignment. If those 2 types are different, you may see errors when replacing all the old calls.

Despatch answered 2/8, 2022 at 15:42 Comment(1)
1+, it happens in javac also, and probably this is the "long" explanation.Ascot
A
4

They differ in what they can process related to inheritance.

This answer really expands this one, since it will be too long for a comment.

Suppose there are two classes:

static class Animal {

}

static class Dog extends Animal {

}

And such a method:

List<Animal> test(List<Dog> dogs) {

    Stream<Dog> stream = dogs.stream();
    Collector<Animal, ?, List<Animal>> collector = Collectors.toList();

    List<Animal> animals = stream.collect(collector);

    return animals;
}

Notice a few probably obvious things:

  • stream is of type Stream<Dog>. The Stream.class is declared as Stream<T> ... , so T will be inferred to be a Dog.

  • stream::collect calls a method that is declared as: <R, A> R collect(Collector<? super T, A, R> collector);

Notice the type: ? super T, that is why we can pass a Collector<Animal, ?, List<Animal>: the first argument Animal is a super type of T (Dog).

This is kind of logic btw if you think what a collector is supposed to do. It must take some type and produce a List of those types (in our case). That "type" in our case is a Dog (but at the same time is an Animal too). The result from the collector is a List<Animal> and you can put dogs in such a List.


The same thing does not work with toList:

List<Animal> test(List<Dog> dogs) {
    Stream<Dog> stream = dogs.stream();
    List<Animal> animals = stream.toList();
    return animals;
}

You probably understand why already, its because Stream::toList is declared as : default List<T> toList() { ... }

Ascot answered 1/12, 2023 at 9:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.