75

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

dreamcrash
  • 47,137
  • 25
  • 94
  • 117
knittl
  • 246,190
  • 53
  • 318
  • 364

3 Answers3

72

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?
Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
  • 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? – knittl Jan 30 '21 at 16:07
  • 16
    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. – Ole V.V. Jan 30 '21 at 18:14
  • 3
    @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.` – Arvind Kumar Avinash Jan 30 '21 at 18:26
  • it's good that you edited, I have no idea how my comment was removed though :| – Eugene Jan 31 '21 at 15:33
  • 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? – Caramiriel Feb 03 '21 at 09:39
  • 1
    @Caramiriel 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. – Ole V.V. Feb 04 '21 at 20:44
  • 14
    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)`. – Brian Goetz Feb 19 '21 at 17:07
  • 5
    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. ) – Clement Cherlin Feb 22 '21 at 14:05
  • 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. https://blogs.oracle.com/javamagazine/hidden-gems-jdk16-jdk17-jep  Can you Validate the above statements in the article ? – deepakl.2000 Jul 28 '21 at 18:44
51

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
collect(toList()) No Yes
collect(toUnmodifiableList()) Yes No
toList() Yes Yes

Another small difference:

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

// Error
List<CharSequence> list = Stream.of("hello", "world").toList();
MC Emperor
  • 22,334
  • 15
  • 80
  • 130
ZhekaKozlov
  • 36,558
  • 20
  • 126
  • 155
  • 2
    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. https://blogs.oracle.com/javamagazine/hidden-gems-jdk16-jdk17-jep Can you Validate the above statements – deepakl.2000 Jul 28 '21 at 18:19
  • 1
    I would add that the first one was introduced in Java 8, the 2nd in Java 10, and the 3rd in Java 16. – Guillaume Husta Dec 05 '22 at 10:05
11

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

Bananeweizen
  • 21,797
  • 8
  • 68
  • 88