HashSet vs TreeSet vs LinkedHashSet in Java: Sets Explained
HashSet is fast and unordered, LinkedHashSet keeps insertion order, TreeSet stays sorted with floor and ceiling. How Java sets find duplicates, and set algebra.
- Course: Java study plan
- Module: Collections framework
- Kind: Lesson
- Reading time: 12 min
- Runtime: OpenJDK 18
What is the difference between HashSet and TreeSet in Java?
HashSet and TreeSet differ in order, speed and how they detect duplicates. HashSet is a hash table: add, remove and contains are O(1) on average, iteration order is unspecified, and membership is decided by hashCode and equals. TreeSet is a sorted red-black tree: O(log n) operations, membership by compareTo, no nulls, and navigation such as floor and ceiling.
Lesson
A Set holds each element at most once, where "once" is decided by equals (and hashCode, or compareTo for tree sets). Three implementations cover almost every need: HashSet for speed, LinkedHashSet when order of insertion matters, TreeSet when elements must be sorted or you need "the smallest element ≥ x". This lesson covers how each decides membership, the set-algebra operations, and the navigation API that makes TreeSet worth its logarithm.
Membership is equals/hashCode (or compareTo)
Set<Point> seen = new HashSet<>();
seen.add(new Point(1, 2));
seen.contains(new Point(1, 2)); // true only if Point overrides equals AND hashCode
HashSet is a HashMap with dummy values: add hashes the element to a bucket and compares with equals against what is there. A class without a proper hashCode puts equal objects in different buckets and the set fills with duplicates. Records and strings are safe; hand-written classes must override both (Module 8).
TreeSet never calls equals or hashCode — it uses compareTo (or its Comparator): two elements comparing as 0 are the same element. new TreeSet<>(List.of(new BigDecimal("2.0"), new BigDecimal("2.00"))).size() is 1, while a HashSet holds 2.
The three implementations
HashSet | LinkedHashSet | TreeSet | |
|---|---|---|---|
| add/contains/remove | O(1) avg | O(1) avg | O(log n) |
| Iteration order | unspecified, may change on resize | insertion order | sorted |
| Nulls | one allowed | one allowed | not allowed |
| Needs | equals/hashCode | equals/hashCode | Comparable or a Comparator |
| Extra | — | predictable output, LRU-ish with access order via LinkedHashMap | first, last, floor, ceiling, headSet, subSet… |
Use HashSet by default. Use LinkedHashSet when the set is shown to a user or compared in tests — unordered output is a source of flaky tests. Use TreeSet when you need sorting or range queries; if you only need the elements sorted once at the end, a HashSet plus one sort is cheaper.
Set algebra
Set<String> a = new HashSet<>(List.of("x", "y", "z"));
Set<String> b = Set.of("y", "z", "w");
Set<String> union = new HashSet<>(a); union.addAll(b); // {x, y, z, w}
Set<String> inter = new HashSet<>(a); inter.retainAll(b); // {y, z}
Set<String> diff = new HashSet<>(a); diff.removeAll(b); // {x}
boolean subset = b.containsAll(inter); // true
boolean disjoint = Collections.disjoint(a, Set.of("q")); // true
The bulk operations mutate their receiver, so copy first when the original must survive. Note add and remove return boolean — whether the set changed — which is the idiom for "have I seen this before":
if (!seen.add(item)) System.out.println("duplicate: " + item);
TreeSet navigation
TreeSet<Integer> t = new TreeSet<>(List.of(10, 20, 30, 40));
t.first(); t.last(); // 10, 40
t.floor(25); t.ceiling(25); // 20, 30 — greatest ≤ / least ≥
t.lower(20); t.higher(20); // 10, 30 — strictly below / above
t.headSet(30); // [10, 20] — view, exclusive
t.tailSet(30); // [30, 40] — view, inclusive
t.subSet(15, 35); // [20, 30] — view [from, to)
t.pollFirst(); t.pollLast(); // remove and return the ends
t.descendingSet(); // a reversed view
new TreeSet<>(Comparator.reverseOrder()); // custom order
new TreeSet<>(String.CASE_INSENSITIVE_ORDER); // "a" and "A" are one element
floor/ceiling in O(log n) are the reason to choose a tree: nearest-neighbour lookups, interval scheduling, "next available slot". The range views are live — modifying them modifies the set, and adding outside the range throws.
Common patterns
// dedupe preserving first occurrence
List<String> unique = new ArrayList<>(new LinkedHashSet<>(items));
// fast membership for a filter
Set<String> stop = Set.of("the", "a", "an");
words.removeIf(stop::contains);
// count distinct
long distinct = new HashSet<>(items).size();
// first duplicate
Set<Integer> seen = new HashSet<>();
for (int x : nums) if (!seen.add(x)) return x;
// sorted unique output
new TreeSet<>(words) // iterate in order
Mutating elements — do not
A HashSet locates elements by the hash computed at insertion; mutate a field that participates in hashCode and the element is in the wrong bucket — contains says false, remove fails, and the set holds a ghost. Same for TreeSet with fields that affect compareTo. Set elements (and map keys) should be immutable: strings, numbers, records, enums.
Immutable sets
Set.of(...) (Java 9): immutable, no nulls, no duplicates (throws IllegalArgumentException on a duplicate literal), iteration order deliberately randomised per JVM run so nobody depends on it. Set.copyOf(collection) copies; Collections.unmodifiableSet(s) is a read-only view of a live set.
Interview angle
- "How does
HashSetcheck membership?"hashCodepicks the bucket,equalsconfirms — so both must be overridden consistently. - "
HashSetvsTreeSet?" O(1) unordered vs O(log n) sorted with navigation;TreeSetneeds comparability and rejects null. - "How do you dedupe a list while keeping order?"
new ArrayList<>(new LinkedHashSet<>(list)). - "What does
addreturn?" Whether the element was absent — the idiom for duplicate detection. - "Why is
Set.ofiteration order unpredictable?" Deliberately randomised so code cannot depend on it.
Key takeaways
HashSet(fast, unordered),LinkedHashSet(insertion order),TreeSet(sorted, navigable, no nulls).- Hash sets need
equals+hashCode; tree sets usecompareToonly — and disagree with hash sets when it is inconsistent withequals. addAll/retainAll/removeAllon a copy for union/intersection/difference;addreturns whether it was new.floor/ceiling/subSetare the tree's payoff. Never mutate elements while they are in a set.
Common questions
How does HashSet check for duplicates?
hashCode picks the bucket and equals confirms a match, since a HashSet is a HashMap with dummy values. Both must be overridden consistently on the element class; without a proper hashCode, equal objects land in different buckets and the set fills with duplicates.
How do you remove duplicates from a list while keeping order in Java?
Pass the list through a LinkedHashSet, which keeps the first occurrence of each element in insertion order: new ArrayList<>(new LinkedHashSet<>(list)). A HashSet would also remove the duplicates but lose the order.
What does Set.add return in Java?
A boolean: true if the element was absent and has been added, false if the set already held an equal element. That makes if (!seen.add(x)) the idiom for detecting a duplicate in one step.
How do you find the union and intersection of two sets in Java?
Copy one set and use the bulk operations: addAll gives the union, retainAll the intersection and removeAll the difference. They modify the set they are called on, so copy first — new HashSet<>(a) — when the original must survive.
Why is the iteration order of Set.of unpredictable?
It is deliberately randomised per JVM run so that no code can come to depend on it. Sets from Set.of are also immutable, reject nulls, and throw IllegalArgumentException when given a duplicate element.
Exercises
Set algebra and navigation
Read two lines of integers (space-separated; the first token of each is the count). Compute with sets: union, intersection and difference (A minus B), printing each as a sorted, space-separated list (use TreeSet for the output order; print an empty line for an empty set). Then read an integer x and print floor=<greatest element of the union ≤ x or none> ceiling=<least element ≥ x or none>.
Example: 3 1 5 9 / 2 5 7, x = 6 →
1 5 7 9
5
1 9
floor=5 ceiling=7Visitors, three ways
Read n lines <day> <visitor>. Print firstSeen=<visitors in first-seen order> (a LinkedHashSet), sorted=<visitors in natural order> (a TreeSet), everyDay=<visitors present on every distinct day> in natural order (intersect per-day HashSets with retainAll), and days=<number of distinct days>.
Example: 4 then mon zed, mon amy, tue amy, tue bob →
firstSeen=[zed, amy, bob]
sorted=[amy, bob, zed]
everyDay=[amy]
days=2In this module: Collections framework
- The collections framework — the map and how to choose
- Lists — ArrayList, LinkedList and the List API
- Sets — HashSet, LinkedHashSet and TreeSet (this lesson)
- Maps — HashMap internals, TreeMap, LinkedHashMap and the modern API
- Queues, deques and PriorityQueue
- Sorting, Comparable and Comparator in depth
- Iterators, fail-fast behaviour and safe modification
- Immutable collections and the utility toolkit
- Checkpoint — Collections framework
← Lists — ArrayList, LinkedList and the List API · Maps — HashMap internals, TreeMap, LinkedHashMap and the modern API →