To convert a Java Stream to List, we call stream.toList() on Java 16+ or stream.collect(Collectors.toList()) on Java 8+. Both calls are terminal operations (that is, the last step of a stream pipeline, which produces the result), so they run the stream and collect its items into a new List in encounter order. The main difference is that the list returned by toList() is unmodifiable, so adding or removing an element throws UnsupportedOperationException. Collectors.toList() returns an ArrayList in practice, so we can modify it, but the Javadoc doesn’t guarantee the list type.
We collect a stream into a list when the result must outlive the pipeline, for example to return search results from a service method or to pass filtered rows to a page template.
The following example collects the same fruits with both methods and shows how each list reacts to add() and to null items.
List<String> fruits = Stream.of("apple", "banana", "cherry").toList(); // [apple, banana, cherry]
List<String> fruitList = Stream.of("apple", "banana", "cherry").collect(Collectors.toList()); // [apple, banana, cherry]
fruitList.add("mango"); // [apple, banana, cherry, mango]
boolean added = fruits.add("mango"); // UnsupportedOperationException
List<String> unmodifiable = Stream.of("apple", null).collect(Collectors.toUnmodifiableList()); // NullPointerException
List<String> withNull = Stream.of("apple", null).toList(); // [apple, null]
Notice that only the list from Collectors.toList() accepts the new item, and only toUnmodifiableList() rejects the null item.
Next, we compare toList(), Collectors.toList() and Collectors.toUnmodifiableList() on modifiability and null items, along with the Java version each one needs. After that, we collect into an ArrayList or LinkedList, filter before collecting, convert an IntStream and collect an infinite stream.
1. Different Ways to Collect Stream Items into List
Java has three ways to collect into a List without naming the list class, and two of them return an unmodifiable list. We can read an unmodifiable list with get() or loop over it, but its add(), remove(), set() and sort() methods throw UnsupportedOperationException.
| Method | Can we change it? | null items | Java version |
|---|---|---|---|
| stream.toList() | No, unmodifiable | Allowed | 16+ |
| stream.collect(Collectors.toUnmodifiableList()) | No, unmodifiable | NullPointerException | 10+ |
| stream.collect(Collectors.toList()) | Yes in practice (ArrayList), not guaranteed | Allowed | 8+ |
| stream.collect(Collectors.toCollection(ArrayList::new)) | Yes, always an ArrayList | Allowed | 8+ |
| stream.collect(Collectors.toCollection(LinkedList::new)) | Yes, always a LinkedList | Allowed | 8+ |
All five keep the encounter order of the stream (that is, the order in which the stream delivers its items). To pick a method, we ask whether our code will change the list and whether the stream can contain null. For example, a REST controller that returns a product list to the client only reads the list, so toList() fits, whereas a service that adds a default item afterwards needs a modifiable list.

1.1. Stream.toList()
Java 16 added toList() to the Stream interface as a default method (that is, a method with a body inside the interface). It is the shortest way to collect a stream, so we use it whenever our code only reads the result.
- The returned List is unmodifiable, so every method that changes the list throws UnsupportedOperationException.
- The List accepts null items, so toList() never fails because of a null in the stream.
- The items keep the order of the stream, if the stream has one.
- The Javadoc does not guarantee any specific list class. On JDK 25, the class is java.util.ImmutableCollections$ListN.
List<String> sorted = Stream.of("cherry", "apple", "banana").sorted().toList(); // [apple, banana, cherry]
String first = sorted.get(0); // "apple"
String replaced = sorted.set(0, "mango"); // UnsupportedOperationException
sorted.sort(null); // UnsupportedOperationException
The exception has no message, so the stack trace is our only hint. Its top frames point to the class ImmutableCollections, which tells us that the list came from toList(), List.of() or Collectors.toUnmodifiableList().
java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:159)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:164)
at com.howtodoinjava.core.streams.conversions.StreamToList.main(StreamToList.java:146)
If our code must change the list later, we use Collectors.toList() or copy the result with new ArrayList<>(list). Other lists, such as the one returned by Arrays.asList(), throw the same exception.
1.2. Stream.collect(Collectors.toUnmodifiableList())
Java 10 added Collectors.toUnmodifiableList(), which returns an unmodifiable list, the same as toList(). The difference is that toUnmodifiableList() rejects null items and throws NullPointerException.
List<String> unmodifiable = Stream.of("apple", "banana", "cherry")
.collect(Collectors.toUnmodifiableList()); // [apple, banana, cherry]
List<String> withNull = Stream.of("apple", null, "cherry").collect(Collectors.toUnmodifiableList()); // NullPointerException
boolean hasNull = unmodifiable.contains(null); // NullPointerException
The last line surprises many developers, because the list throws NullPointerException even when we only ask whether it contains null, and List.of() behaves the same way. We use toUnmodifiableList() on Java 10 to 15, or when a null item is a bug that should fail early.
The matching collectors toUnmodifiableSet() and toUnmodifiableMap() collect a stream into an immutable set or map.
1.3. Stream.collect(Collectors.toList())
Java 8 added Collectors.toList() together with the original Stream API. We can add, remove and sort the items of the returned list, and it accepts null items.
List<String> fruitList = Stream.of("apple", "banana", "cherry")
.collect(Collectors.toList()); // [apple, banana, cherry]
fruitList.add("mango"); // [apple, banana, cherry, mango]
String className = fruitList.getClass().getName(); // "java.util.ArrayList"
List<String> withNull = Stream.of("apple", null, "cherry").collect(Collectors.toList()); // [apple, null, cherry]
Every JDK so far returns an ArrayList. However, the Javadoc gives “no guarantees on the type, mutability, serializability, or thread-safety” of the list. So when our code must add items, Collectors.toCollection(ArrayList::new) is the safer choice.
2. Collecting Stream into ArrayList or LinkedList
Collectors.toCollection() lets us choose the list class. We pass it a Supplier (a function that creates the empty collection), and the collector adds every stream item to that collection. The usual supplier is a constructor reference such as ArrayList::new, which is a short way to write () -> new ArrayList<>().
ArrayList<String> arrayList = Stream.of("apple", "banana", "cherry")
.collect(Collectors.toCollection(ArrayList::new)); // [apple, banana, cherry]
LinkedList<String> linkedList = Stream.of("apple", "banana", "cherry")
.collect(Collectors.toCollection(LinkedList::new)); // [apple, banana, cherry]
linkedList.addFirst("mango"); // [mango, apple, banana, cherry]
Because the variable can use the class itself as its type, such as LinkedList<String>, we can call methods of that class without a cast, for example addFirst() of LinkedList or ensureCapacity() of ArrayList.
3. Filtering a Stream and Collecting Items into a List
When we need only some items from the Stream, we call Stream.filter() before collecting. It takes a predicate (a function that returns true for each item to keep), and toList() collects the remaining items.
For example, a grocery app shows a deals section with the fruits that cost less than 6, where Fruit is a record (a short data class) with a name and a price.
record Fruit(String name, int price) {}
List<Fruit> cheapFruits = Stream.of(
new Fruit("apple", 5),
new Fruit("banana", 3),
new Fruit("cherry", 9),
new Fruit("mango", 7))
.filter(f -> f.price() < 6)
.toList(); // [Fruit[name=apple, price=5], Fruit[name=banana, price=3]]
To collect only one field, such as the name, we add map() before toList(), which turns each Fruit into its name.
List<String> cheapNames = Stream.of(new Fruit("apple", 5), new Fruit("cherry", 9))
.filter(f -> f.price() < 6)
.map(Fruit::name)
.toList(); // [apple]
4. Converting an IntStream to List
IntStream, LongStream and DoubleStream are primitive streams, which hold int, long or double values instead of objects. Because a List can hold only objects, a primitive stream has no collect(Collectors.toList()) method. So we first turn each int into an Integer with boxed(), or into any other object with mapToObj().
List<Integer> numbers = IntStream.rangeClosed(1, 5).boxed().toList(); // [1, 2, 3, 4, 5]
List<String> labels = IntStream.rangeClosed(1, 3).mapToObj(i -> "item-" + i).toList(); // [item-1, item-2, item-3]
List<Integer> fromArray = Arrays.stream(new int[]{4, 8, 15}).boxed().toList(); // [4, 8, 15]
Calling IntStream.toList() without boxed() does not compile, because IntStream has no toList() method.
5. Collect Items from Infinite Stream into List
An infinite stream never runs out of items, so we must limit it to a fixed number of items before collecting it. Without a limit, toList() never returns, and the program fails with OutOfMemoryError once the heap is full.
List<Integer> firstTen = IntStream.iterate(1, i -> i + 1)
.limit(10)
.boxed()
.toList(); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
List<Integer> evens = Stream.iterate(2, i -> i <= 10, i -> i + 2).toList(); // [2, 4, 6, 8, 10]
The second line uses the version of iterate() that takes a condition, added in Java 9. The stream stops when the condition becomes false, so here it stops once i goes past 10.
6. Java Stream to List FAQs
Most of these questions come up when teams move old code from Collectors.toList() to toList(), or when a converted list behaves differently than expected.
6.1. What Is the Difference Between Stream.toList() and Collectors.toList()?
The main difference is that the list returned by toList() is unmodifiable, so adding or removing an element throws UnsupportedOperationException. Collectors.toList() returns an ArrayList in practice, so we can modify it, but the Javadoc doesn’t guarantee the list type. Stream.toList() needs Java 16, whereas Collectors.toList() works on Java 8, and both accept null items and keep the encounter order.
String toListClass = Stream.of("apple").toList().getClass().getName(); // "java.util.ImmutableCollections$ListN"
String collectorClass = Stream.of("apple").collect(Collectors.toList()).getClass().getName(); // "java.util.ArrayList"

6.2. Can We Replace collect(Collectors.toList()) With toList()?
Yes, when our code never changes the list afterwards, and IntelliJ IDEA even suggests the replacement. The change is not safe if later code calls add(), remove(), set() or sort() on the list, or passes it to a method that does. These calls compile but throw UnsupportedOperationException at runtime, so we search for writes to the list before we replace the call.
6.3. Does toList() Keep the Order in a Parallel Stream?
Yes. Both toList() and Collectors.toList() return the items in encounter order, even when a parallel stream processes them on several threads.
List<Integer> parallel = IntStream.rangeClosed(1, 10).parallel().boxed().toList(); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
A stream from a HashSet has no encounter order, so the list follows the order in which the set returns its items.
6.4. Why Does a Second toList() Call Throw IllegalStateException?
Because a stream can be used only once, any call on the same Stream object after the first terminal operation, such as toList(), throws IllegalStateException.
Stream<String> stream = Stream.of("apple", "banana");
List<String> first = stream.toList(); // [apple, banana]
List<String> second = stream.toList(); // IllegalStateException: stream has already been operated upon or closed
To fix the error, we keep the List from the first call, or we create a new stream from the source each time.
6.5. How Do We Add Stream Items to an Existing List?
We collect the stream first and pass the result to addAll(). For example, a shopping app keeps the items already in the cart and adds the items of a saved wish list. Calling forEach(cart::add) also works on a sequential stream, but on a parallel stream several threads call add() on the same ArrayList at once and items get lost.
List<String> cart = new ArrayList<>(List.of("bread"));
boolean changed = cart.addAll(Stream.of("milk", "eggs").toList()); // true
List<String> merged = cart; // [bread, milk, eggs]
6.6. How Do We Convert a Stream to an Array or a Map Instead?
Other targets follow the same pattern, where toArray() returns an array, and Collectors.toMap() builds a Map from two functions, one that makes the key and one that makes the value.
7. Conclusion
We use Stream.toList() for lists we only read, and Stream.collect(Collectors.toList()) for lists we change. The collector Collectors.toUnmodifiableList() fits Java 10 to 15 and streams that must not contain null. When we need a guaranteed ArrayList, LinkedList or another List class, we use Collectors.toCollection(ArrayList::new) or Collectors.toCollection(LinkedList::new).
8. References
Happy Learning !!
Awesome content. It is very useful for learn the fundamentals