Back to Blog
Java

Java List vs Set: Key Differences and When to Use Each

java list vs set: Compare Java List and Set: ordering, duplicates, performance, and implementation tradeoffs. Learn which collection fits your data model.

java collectionslist vs setarraylisthashsetdata structuresjava performance
Diagram contrasting an ordered List with duplicate elements against a Set that enforces uniqueness

The java list vs set decision comes down to two guarantees. A List preserves insertion order and permits duplicate elements. A Set rejects duplicates and, in its most common implementation, makes no promise about iteration order. Both are Collection interfaces, but they model different data semantics. Choosing the wrong one leads to subtle bugs: duplicate entries appearing where uniqueness was expected, or iteration order changing between runs.

What List Guarantees

List is an ordered sequence. Every element has a positional index, and the interface exposes methods that depend on that index: get(int), set(int, E), add(int, E), and remove(int). Duplicates are allowed because each occurrence is a distinct element at a distinct position.

List<String> names = new ArrayList<>(); names.add("Ada"); names.add("Grace"); names.add("Ada"); // allowed, second occurrence System.out.println(names.get(0)); // Ada System.out.println(names.size()); // 3

The list above contains "Ada" twice. That is valid because the contract of List does not restrict duplicate values. If your domain requires that a name appear only once, a List will not enforce that rule for you.

What Set Guarantees

Set models a mathematical set: no duplicate elements. The uniqueness check is performed through equals() and hashCode(). When you add an element that is already present, the add method returns false and the collection is unchanged.

Set<String> names = new HashSet<>(); names.add("Ada"); names.add("Grace"); boolean added = names.add("Ada"); // false, already present System.out.println(names.size()); // 2

The critical detail is that HashSet does not guarantee iteration order. The order depends on hash values and the internal bucket layout, which can change when the set is resized. If you need both uniqueness and insertion order, LinkedHashSet provides that combination.

Implementation Classes and Their Tradeoffs

The interface you choose is only half the decision. Each interface has multiple implementations with different runtime characteristics.

List implementations

ArrayList is backed by a resizable array. Index access is O(1), and appending at the end is amortized O(1). Inserting or removing at an arbitrary index requires shifting elements, which is O(n). LinkedList is a doubly-linked list: inserting or removing at either end is O(1), but accessing an element by index is O(n) because the list must be traversed.

Set implementations

HashSet uses a hash table. add, remove, and contains are O(1) on average, assuming a well-distributed hash function. LinkedHashSet extends HashSet with a linked list that preserves insertion order, at the cost of slightly higher memory usage. TreeSet is backed by a red-black tree and keeps elements sorted according to their natural ordering or a Comparator. Its operations are O(log n).

ImplementationOrderingadd / containsMemory overhead
ArrayListinsertion orderO(1) append, O(n) containslow
LinkedListinsertion orderO(1) ends, O(n) indexmoderate
HashSetnone guaranteedO(1) averagelow
LinkedHashSetinsertion orderO(1) averagemoderate
TreeSetsortedO(log n)moderate

Performance: The Contains Operation

The most decisive performance difference between List and Set is the contains operation. A List has no index structure for values, so contains performs a linear scan. A HashSet computes the element's hash and probes the bucket directly.

// Linear scan: O(n) boolean inList = list.contains(target); // Hash lookup: O(1) average boolean inSet = set.contains(target);

For a collection with thousands of elements, repeated contains calls on a List become a measurable bottleneck. If membership testing is a frequent operation, a Set is the appropriate structure regardless of whether you also need to store the elements in a list.

When to Choose List

Use a List when position is part of the data model. If you need to retrieve the third element, replace an element at a specific index, or iterate in the exact order elements were added, List is the correct interface. Duplicates are also a signal: a shopping cart, a log of events, or a queue of pending tasks all legitimately contain repeated values.

List also supports ListIterator, which allows bidirectional traversal and modification during iteration. That capability is not available on Set.

When to Choose Set

Use a Set when uniqueness is a domain rule. User IDs, email addresses, and configuration keys are naturally unique. A Set enforces that invariant at the collection level, so callers cannot accidentally introduce duplicates.

Set also enables efficient set algebra. The Set interface supports addAll, retainAll, and removeAll, which implement union, intersection, and difference. These operations are far more readable than manual loops over two lists.

Set<String> admins = new HashSet<>(List.of("ada", "grace")); Set<String> active = new HashSet<>(List.of("ada", "linus")); Set<String> intersection = new HashSet<>(admins); intersection.retainAll(active); // {ada}

Converting Between List and Set

Conversion is straightforward, but it is not lossless in both directions. Passing a List to a HashSet constructor removes duplicates and discards ordering. Passing a Set to an ArrayList constructor preserves the set's iteration order, which is undefined for HashSet and sorted for TreeSet.

List<String> withDuplicates = List.of("a", "b", "a"); Set<String> unique = new HashSet<>(withDuplicates); // {a, b} Set<String> source = new LinkedHashSet<>(List.of("x", "y")); List<String> backToList = new ArrayList<>(source); // [x, y]

If you need to preserve insertion order during conversion, use LinkedHashSet as the target set. If you need a sorted list, use TreeSet or sort the list explicitly.

The equals() and hashCode() Contract

Set correctness depends entirely on consistent equals() and hashCode() implementations. If two objects are equal according to equals(), they must produce the same hash code. Violating this rule breaks deduplication: two logically identical elements will both be stored.

A subtler failure occurs when an object is mutated after being added to a HashSet. If the mutation changes the object's hash code, the element remains in the old bucket and becomes unreachable through contains or remove. The set silently leaks the element. This is why elements stored in a HashSet should be effectively immutable, or at least never mutated in a way that changes their hash.

class MutableKey { int id; // hashCode() depends on id } MutableKey key = new MutableKey(); key.id = 1; Set<MutableKey> set = new HashSet<>(); set.add(key); key.id = 2; // hash changes, element is now lost in the set

A Practical Decision Framework

The decision is not about which interface is "better." It is about which invariant your data requires. If the data has a positional meaning or permits duplicates, use List. If the data must be unique and membership tests are common, use Set.

For the implementation, start with ArrayList and HashSet. They are the default choices with the best average-case behavior. Switch to LinkedHashSet only when you need insertion order alongside uniqueness, and switch to TreeSet only when you need sorted iteration. Choosing LinkedList is rarely justified unless you are doing many insertions and removals at both ends and profiling shows it matters.

java list vs set: Practical Usage and Code Examples | RYUSLOG DEV