Detailed explanation
Collectors.toList() and Collectors.toSet() are collectors used with Stream.collect() to accumulate stream elements into different collection types. toList() produces a List, preserving duplicates and the stream’s encounter order when one exists. A List also supports index-based access through get(index). toSet() produces a Set, eliminating duplicates without guaranteeing iteration order. For example, collecting ["Ravi", "Anu", "Ravi", "Zoya"] into a list retains all four elements in that order; collecting into a set retains three distinct names, whose iteration order is unspecified.
Under the hood, collectors accumulate elements into result containers and combine partial results when necessary, including during parallel execution. Current standard JDK implementations use ArrayList for toList() and HashSet for toSet(), but neither collector guarantees its concrete implementation, mutability, serializability, or thread-safety. Therefore, avoid casting the result to those implementation classes. When a particular collection is required, use Collectors.toCollection(), for example toCollection(LinkedList::new). toSet() is an UNORDERED collector; toList() must respect an existing encounter order even in parallel. Neither collector is a concurrent collector, and parallel execution does not make its result thread-safe.
Set uniqueness follows equality semantics. With the usual HashSet implementation, hashCode() locates candidate buckets and equals() identifies equal elements, so custom classes must implement both consistently. Without overrides, distinct POJO instances remain distinct even when their fields match; repeated references to the same object still deduplicate. Strings and records provide value-based equality. HashSet.contains() is typically O(1) on average with suitable hashing, whereas ArrayList.contains() is O(n); these are implementation characteristics, not universal collector guarantees. For ordered deduplication, use toCollection(LinkedHashSet::new). For sorted uniqueness, use toCollection(TreeSet::new), where comparison determines duplicates instead of equals().
Interviewers mainly expect you to choose based on duplicates, order, and downstream operations, while distinguishing API contracts from implementation details. Neither collector promises a modifiable result; use toCollection(ArrayList::new) when that guarantee matters. Java 10 introduced toUnmodifiableList() and toUnmodifiableSet(), which reject nulls. Java 16’s Stream.toList() is different from Collectors.toList(): it guarantees an unmodifiable list and permits null elements. The usual HashSet permits one null; a naturally ordered TreeSet rejects null, although a null-friendly comparator can allow it.
Key points to remember
- toList() retains duplicates, preserves an existing encounter order, and supports indexed access.
- toSet() removes duplicates and gives no iteration-order guarantee.
- ArrayList and HashSet are current implementation choices, not guaranteed result types.
- Use toCollection(LinkedHashSet::new) for unique elements in encounter order.
- Implement equals() and hashCode() consistently for value-based deduplication in hash-based sets.
Common mistakes candidates make
- Assuming Collectors.toList() guarantees a mutable ArrayList, or confusing it with the unmodifiable Stream.toList().
- Expecting toSet() to preserve insertion order or automatically sort elements.
- Assuming different POJO instances with identical fields deduplicate without suitable equality methods.
Likely follow-up questions
How do you remove duplicates while preserving the original order?
Use stream.collect(Collectors.toCollection(LinkedHashSet::new)). For a stream with encounter order, the resulting set iterates distinct elements in their first-encounter order.
How is Stream.toList() different from Collectors.toList()?
Stream.toList(), available since Java 16, guarantees an unmodifiable list and allows null elements. Collectors.toList(), available since Java 8, makes no mutability guarantee; both preserve an existing encounter order.
Should you always use toSet() for faster membership checks?
A HashSet is usually suitable for repeated contains() checks because lookup is O(1) on average with suitable hashing, whereas an ArrayList requires a linear scan. However, toSet() does not guarantee HashSet, so use toCollection(HashSet::new) if that implementation is a requirement.
Interview tip: Start with duplicates and encounter order, give the repeated-name example, and finish with LinkedHashSet for ordered deduplication.
