HuntExams Academy logo
JavaBeginner#streams

What is the difference between Collectors.toList() and Collectors.toSet()?

Answer

Collectors.toList() collects stream elements into a List \u2014 encounter order preserved, index access, duplicates kept. Collectors.toSet() collects into a Set \u2014 duplicates removed using equals() and hashCode(), with no order guarantee. Details that matter: - Implementation: toList() currently returns an ArrayList and toSet() a HashSet, but the API guarantees neither the type, nor mutability, nor serializability, nor thread-safety. For a specific type use toCollection(LinkedList::new) or toCollection(TreeSet::new). - Order: a List keeps encounter order; HashSet does not. Use toCollection(LinkedHashSet::new) when you want deduplication AND order \u2014 a very common real requirement. - Deduplication depends entirely on correct equals() and hashCode(). Records and String get this right automatically; a plain POJO without overrides deduplicates nothing, because it falls back to identity. - Lookups: set.contains() is O(1) on average, list.contains() is O(n) \u2014 so prefer toSet() when the result feeds membership checks. - Nulls: HashSet allows a single null; TreeSet throws NullPointerException on null. - Mutability: Java 10 added toUnmodifiableList() and toUnmodifiableSet(). Java 16+ has the shorter stream.toList(), which returns an unmodifiable list that does allow nulls, unlike List.copyOf. - Parallel streams: toSet() is UNORDERED and can merge slightly more cheaply, while toList() must preserve order. Rule of thumb: need order or duplicates, use toList(); need uniqueness or fast contains, use toSet(); need both, use toCollection(LinkedHashSet::new).

Example

List<String> names = List.of("Ravi", "Anu", "Ravi", "Zoya");

List<String> asList = names.stream().collect(Collectors.toList());
// [Ravi, Anu, Ravi, Zoya] -- order kept, duplicate kept

Set<String> asSet = names.stream().collect(Collectors.toSet());
// [Anu, Ravi, Zoya] in unspecified order -- duplicate removed

// Deduplicate but keep order
Set<String> ordered = names.stream()
    .collect(Collectors.toCollection(LinkedHashSet::new));   // [Ravi, Anu, Zoya]

// Sorted and unique
Set<String> sorted = names.stream()
    .collect(Collectors.toCollection(TreeSet::new));         // [Anu, Ravi, Zoya]

// Java 16+ shorthand (unmodifiable)
List<String> immutable = names.stream().map(String::trim).toList();

// Deduplication needs equals/hashCode
record Student(int id, String name) {}   // record: generated, works
// class Student { int id; }              // no overrides: nothing dedupes

// Set for fast membership checks
Set<String> blocked = loadBlocked().stream().collect(Collectors.toSet());
if (blocked.contains(user)) { /* O(1) */ }

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.

Related Interview Questions

1
JavaIntermediate#streams

How do you group elements using Collectors.groupingBy?

Open
2
JavaIntermediate#streams

What is the difference between findFirst() and findAny() in streams?

Open
3
JavaBeginner#java8

What is the difference between Optional.of() and Optional.ofNullable()?

Open