Why this matters
- A
Setis the clearest way to express "membership matters, duplicates do not", and using aListwithcontainsinstead is a common and avoidable O(n) mistake. - Which set you pick determines whether iteration order is arbitrary, insertion-based or sorted, and that silently affects test reliability.
- Uniqueness depends entirely on
equalsandhashCode, so a broken contract shows up here first.
Three implementations
| Aspect | Implementation | Properties |
|---|---|---|
| HashSet | No order guarantee | O(1) add, remove, contains |
| LinkedHashSet | Insertion order preserved | O(1), slightly more memory for the links |
| TreeSet | Sorted order | O(log n), plus navigation and range queries |
HashSet
ImplementationNo order guaranteePropertiesO(1) add, remove, containsLinkedHashSet
ImplementationInsertion order preservedPropertiesO(1), slightly more memory for the linksTreeSet
ImplementationSorted orderPropertiesO(log n), plus navigation and range queries
Each wraps a map internally: HashSet over HashMap, LinkedHashSet over LinkedHashMap, TreeSet over TreeMap.
List<String> input = List.of("carl", "ana", "bob", "ana");
Set<String> hash = new HashSet<>(input);
Set<String> linked = new LinkedHashSet<>(input);
Set<String> tree = new TreeSet<>(input);
System.out.println(hash); // [bob, ana, carl] — arbitrary, may differ between runs
System.out.println(linked); // [carl, ana, bob] — first-seen order
System.out.println(tree); // [ana, bob, carl] — sorted
Each dropped the duplicate "ana". Only the ordering differs.
Uniqueness depends on your contract
A HashSet decides whether an element is already present by hashing it and then comparing with
equals. A broken contract breaks the set.
class Tag {
final String name;
Tag(String name) { this.name = name; }
// equals and hashCode deliberately not overridden
}
Set<Tag> tags = new HashSet<>();
tags.add(new Tag("java"));
tags.add(new Tag("java"));
System.out.println(tags.size()); // 2 — identity comparison, so both are "unique"
record Tag(String name) { } // equals and hashCode generated from the component
Set<Tag> tags = new HashSet<>();
tags.add(new Tag("java"));
tags.add(new Tag("java"));
System.out.println(tags.size()); // 1 — as expected
TreeSet uses compareTo or a supplied Comparator instead, so it has the same dependency on a
different method. Two elements comparing as 0 are considered duplicates even if equals disagrees.
TreeSet navigation
Sorted order unlocks operations the other two cannot offer, defined by the NavigableSet interface.
NavigableSet<Integer> scores = new TreeSet<>(List.of(10, 20, 30, 40, 50));
scores.first(); // 10
scores.last(); // 50
scores.floor(35); // 30 — greatest element ≤ 35
scores.ceiling(35); // 40 — least element ≥ 35
scores.lower(30); // 20 — strictly less than
scores.higher(30); // 40 — strictly greater than
scores.headSet(30); // [10, 20] — exclusive by default
scores.tailSet(30); // [30, 40, 50] — inclusive by default
scores.subSet(20, 45); // [20, 30, 40]
scores.descendingSet(); // [50, 40, 30, 20, 10] — a view, not a copy
scores.pollFirst(); // removes and returns 10
scores.pollLast(); // removes and returns 50
These are the reason to choose TreeSet despite its O(log n) cost. "The nearest value at or below
this" is not expressible with a hash set at all.
// A custom ordering, longest first then alphabetical
Set<String> byLength = new TreeSet<>(
Comparator.comparingInt(String::length).reversed()
.thenComparing(Comparator.naturalOrder()));
byLength.addAll(List.of("java", "go", "kotlin", "rust"));
System.out.println(byLength); // [kotlin, java, rust, go]
Set algebra
The bulk operations give you union, intersection and difference directly.
Set<String> backend = new HashSet<>(Set.of("java", "go", "sql"));
Set<String> frontend = new HashSet<>(Set.of("js", "css", "sql"));
Set<String> union = new HashSet<>(backend);
union.addAll(frontend); // [java, go, sql, js, css]
Set<String> intersection = new HashSet<>(backend);
intersection.retainAll(frontend); // [sql]
Set<String> difference = new HashSet<>(backend);
difference.removeAll(frontend); // [java, go]
boolean subset = backend.containsAll(Set.of("java", "sql")); // true
Each of these mutates the receiver, which is why the copy comes first. Forgetting the copy destroys the original set — an easy mistake in a method that was only supposed to compute something.
EnumSet for enum keys
When the elements are enum constants, there is a specialised implementation that is dramatically more efficient.
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
Set<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
Set<Day> weekdays = EnumSet.complementOf(EnumSet.copyOf(weekend));
Set<Day> midweek = EnumSet.range(Day.TUE, Day.THU);
System.out.println(weekdays); // [MON, TUE, WED, THU, FRI] — declaration order
EnumSet represents membership as bits in a single long when the enum has 64 or fewer constants. Every
operation becomes a bitwise instruction, and the whole set occupies a handful of bytes. It iterates in
declaration order, which is usually the order you want.
Choosing
HashSet— the default. Fastest, least memory, no ordering promise.LinkedHashSet— when iteration order must match insertion order, including in tests.TreeSet— when you need sorted iteration or navigation. Note that it rejectsnull.EnumSet— always, for enum elements.CopyOnWriteArraySet— read-heavy concurrent use. It is backed by an array, socontainsis O(n).ConcurrentHashMap.newKeySet()— the concurrent set to reach for when writes are frequent.
Common misreadings
- "
HashSetkeeps insertion order." It does not. The order you observe is an accident of hashing. - "A
Setcannot containnull."HashSetandLinkedHashSetaccept one.TreeSetrejects it, because comparing requires a non-null value. - "
TreeSetusesequals." It usescompareToor aComparator. Elements comparing as0are duplicates regardless ofequals. - "
retainAllreturns the intersection." It modifies the receiver and returns a boolean. Copy first. - "
Sethasget(index)." It has no positional access. Iterate, or use aListif position matters. - "
HashSetadds no memory over a list." It is backed by aHashMap, so each element carries an entry object.
Quick recall
Everything you need if you only revisit this box.
- All three sets guarantee uniqueness; they differ in ordering and cost.
HashSetO(1) and unordered,LinkedHashSetO(1) and insertion-ordered,TreeSetO(log n) and sorted.- Each wraps the matching map internally.
- Uniqueness relies on
equals/hashCode— or oncompareTo/ComparatorforTreeSet, where comparing to0means duplicate. - Never assert on
HashSetiteration order; useLinkedHashSetor sort. TreeSetaddsfloor,ceiling,headSet,tailSet,subSet,descendingSet,pollFirst/Last.addAll,retainAllandremoveAllgive union, intersection and difference — but they mutate the receiver, so copy first.- Use
EnumSetfor enum elements: bitwise operations, tiny footprint, declaration order.
Test yourself
Answer these before moving on — recall is what makes it stick.