Before the questions, make sure you can: explain what "no duplicates" means for a Set and predict the iteration order of HashSet, LinkedHashSet and TreeSet; state the equals/hashCode contract, explain how a hash table uses hashCode to pick a bucket and equals to compare inside it, and describe exactly what breaks when a class overrides only one of them; build a TreeSet or TreeMap with Comparable or a Comparator and explain why a comparator that returns 0 makes the tree drop an element; use the navigation methods first, last, floor, ceiling, lower, higher, headSet and tailMap and say which bounds are inclusive; use the Map API fluently (put's return value, getOrDefault, merge, computeIfAbsent, iteration over entrySet); choose between HashMap, LinkedHashMap and TreeMap from their ordering, null rules and costs; and write and trace the classic patterns — frequency counting, grouping into Map<K, List<V>>, first repetition, and union/intersection/difference with addAll/retainAll/removeAll.
A list answers "what is at position 3?". A set and a map answer different questions: "is this item here?" and "what belongs to this key?". Think of a party guest list (a set: each name appears once, you only check whether someone is on it) and your phone's contacts (a map: you look up a name and get a number). This chapter shows the three ways Java stores sets and maps — hashing, hashing plus a linked list, and a sorted tree — and how to pick the right one. Almost every real program uses these two structures, so they are worth learning well.
Sets: collections without duplicates
A set is a guest list. If "Ana" is already on the list, writing "Ana" again changes nothing. The doorman only asks one question: "Is this person on the list?"
A set is a collection in which every element appears at most once. The Set<E> interface adds no new methods to Collection<E>; it changes the contract (the promise the methods make): add refuses an element that is already present and tells you so through its boolean result.
Set<String> seen = new HashSet<>();
System.out.println(seen.add("red")); // true - added
System.out.println(seen.add("red")); // false - already there, set unchanged
System.out.println(seen.size()); // 1
Worked example. Add five names to a LinkedHashSet (it keeps arrival order, so the printout is predictable):
| Step | Call | Returns | Set after the step | Why |
|---|---|---|---|---|
| 1 | add("Ana") |
true |
[Ana] | new name |
| 2 | add("Ben") |
true |
[Ana, Ben] | new name |
| 3 | add("Ana") |
false |
[Ana, Ben] | Ana is already there |
| 4 | add("Chen") |
true |
[Ana, Ben, Chen] | new name |
| 5 | add("Ben") |
false |
[Ana, Ben, Chen] | Ben is already there |
Five calls, but size() is 3.
"Already present" does not mean "the same object". It means equal according to the set's own rule: equals/hashCode for hash-based sets, compareTo/compare for tree-based sets. Everything in this chapter follows from that one sentence.
A set has no index: there is no get(int). You ask questions (contains), you add and remove, and you iterate.
- A
Listkeeps duplicates and positions:[red, red]has size 2 andget(0)works. - A
Setkeeps neither: no duplicates, noget(int). - Choose a set when your main question is "have I seen this before?" — with a
HashSetit is answered in O(1) expected time, whilelist.contains(x)on anArrayListscans every element, Θ(n).
Three implementations, three orders
Imagine three ways to keep the guest list. HashSet: names are dropped into numbered boxes by a formula — very fast, but when you read the boxes in order the names look shuffled. LinkedHashSet: the same boxes, plus a string that ties the names together in the order they arrived. TreeSet: a list that the secretary keeps in alphabetical order at all times — a little slower, but always sorted.
| Class | Built on | Iteration order | add / contains / remove |
|---|---|---|---|
HashSet |
hash table | unspecified — may look random and may change as the set grows | O(1) expected |
LinkedHashSet |
hash table + linked list | insertion order (re-adding an element does not move it) | O(1) expected |
TreeSet |
red-black tree | sorted (natural order or a Comparator) |
O(log n) guaranteed |
int[] data = {5, 1, 3, 1, 9, 5};
Set<Integer> linked = new LinkedHashSet<>();
Set<Integer> tree = new TreeSet<>();
for (int d : data) { linked.add(d); tree.add(d); }
System.out.println(linked); // [5, 1, 3, 9]
System.out.println(tree); // [1, 3, 5, 9]
Step by step — the state of both sets after each add:
| Added | LinkedHashSet |
TreeSet |
Note |
|---|---|---|---|
| 5 | [5] | [5] | |
| 1 | [5, 1] | [1, 5] | tree puts 1 before 5 |
| 3 | [5, 1, 3] | [1, 3, 5] | |
| 1 | [5, 1, 3] | [1, 3, 5] | duplicate: nothing changes |
| 9 | [5, 1, 3, 9] | [1, 3, 5, 9] | |
| 5 | [5, 1, 3, 9] | [1, 3, 5, 9] | duplicate: 5 does not move to the end |
Never write code — or tests — that depends on the order of a HashSet. If the order matters, say so in the type you choose.
Choose HashSet when you only ask "is it there?", LinkedHashSet when you also want to remember the order things arrived, and TreeSet when you need sorted order or "nearest element" questions.
The equals/hashCode contract
A hash table works like a coat check. When you hand in your coat, you get a ticket number (the hash code), and the coat goes on hook number "ticket mod number of hooks" (the bucket). To find your coat, the attendant goes straight to that hook and then looks at the few coats hanging there (equals). If two equal coats could get different ticket numbers, the attendant would look on the wrong hook.
A HashSet (and a HashMap) finds an element in two steps: hashCode() chooses a bucket, then equals() is used to compare only with the elements in that bucket. For this to work, Object defines a contract:
- If
a.equals(b)istrue, thena.hashCode() == b.hashCode()must hold. - If the hash codes are equal, the objects may still be different (a collision is allowed).
hashCode()must return the same value while the fields used byequalsdo not change.
public final class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return 31 * x + y; // or Objects.hash(x, y)
}
}
Worked example. Take a table with 16 buckets and, to keep it simple, bucket = hash code mod 16. (The real HashMap mixes the bits a little first, but the idea is the same; Chapter 14 shows the details.)
| Point | hashCode = 31·x + y | Bucket (mod 16) |
|---|---|---|
| (1, 2) | 33 | 1 |
| (2, 1) | 63 | 15 |
| (0, 33) | 33 | 1 — a collision with (1, 2) |
bucket: 0 1 2 ... 15
[ ] [(1,2) -> (0,33)] [ ] [(2,1)]
Now contains(new Point(1, 2)):
- Compute the hash code: 33. Go to bucket 1. The other 15 buckets are never looked at.
- Compare with
(1, 2)usingequals→true. Found.
Point (0, 33) is in the same bucket but equals says it is different, so it is not confused with (1, 2). That is rule 2: a collision costs a little time, never a wrong answer.
What goes wrong when you override only one of them?
| You override… | Effect in a HashSet |
|---|---|
equals only |
Two equal points usually get different identity hash codes, land in different buckets and are never compared: the set keeps "duplicates", and contains(new Point(1, 2)) is usually false. |
hashCode only |
Equal points share a bucket, but Object.equals compares references, so they are still considered different: duplicates again, and contains of a new equal object is false. |
| both, consistently | Works as expected. |
In the coat-check picture: overriding only equals is an attendant who can recognise your coat but walks to a random hook; overriding only hashCode is an attendant who walks to the right hook but only accepts the very same ticket paper, not a copy with the same number.
- Changing a field that
hashCodeuses after the object is in aHashSetor used as aHashMapkey. The object stays in its old bucket, socontainsandremovecan no longer find it. Keys should be immutable (String,Integer, or your ownfinalclasses). - Writing
equals(Point p)instead ofequals(Object o). That is an overload, not an override; collections callequals(Object)and never see it.@Overridecatches this at compile time. - Believing "equal hash codes means equal objects". Only the other direction is guaranteed: equal objects ⇒ equal hash codes.
Ordering: Comparable and Comparator in a TreeSet
A TreeSet is like a librarian who keeps the books sorted on the shelf. To place a new book, the librarian only asks one question again and again: "Does it go before, after, or is it the same as this book?" The answer comes from compareTo or from a Comparator. If the answer is "the same", the new book is not shelved.
A TreeSet needs to compare elements. It uses either:
- the elements' natural order — the class implements
Comparable<T>(String,Integer,LocalDate…), or - a
Comparator<T>passed to the constructor.
If you give neither and the class is not Comparable, the code compiles (the constructor cannot check), but the first add throws ClassCastException.
String's natural order compares UTF-16 code units, so upper-case letters come before lower-case ones: [Apple, Banana, apple, banana].
The crucial rule: a TreeSet never calls equals. Two elements for which compare returns 0 are the same element as far as the tree is concerned, and the second one is silently dropped.
Set<String> byLength = new TreeSet<>(Comparator.comparingInt(String::length));
byLength.add("cat");
byLength.add("dog"); // length 3 again: compare returns 0 -> not added
byLength.add("ox");
System.out.println(byLength); // [ox, cat]
Step by step with two more words:
| Call | compare says | Returns | Set after |
|---|---|---|---|
add("cat") |
(empty tree) | true |
[cat] |
add("dog") |
length 3 vs 3 → 0, "same" | false |
[cat] |
add("ox") |
length 2 < 3 → before | true |
[ox, cat] |
add("horse") |
length 5 > 3 → after | true |
[ox, cat, horse] |
add("pig") |
length 3 vs 3 → 0, "same" | false |
[ox, cat, horse] |
"dog" and "pig" are not equal to "cat", but the tree never asks equals, so they are lost.
This is why the API documentation asks for compareTo to be consistent with equals: a.compareTo(b) == 0 exactly when a.equals(b). If you really want to sort by length, add a tie-breaker so that only truly equal strings compare as 0:
Comparator<String> c = Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder());
With this comparator the same five words give [ox, cat, dog, pig, horse]: sorted by length, and alphabetically inside each length.
Sometimes an inconsistent comparator is exactly what you want: new TreeSet<>(String.CASE_INSENSITIVE_ORDER) keeps one spelling of each word, whatever its case. Adding "Java", "java", "JAVA", "Python" gives [Java, Python] — the first spelling wins.
NavigableSet and NavigableMap
Because a tree keeps everything sorted, it can answer "nearest" questions, like asking a shop assistant: "What is the cheapest phone that costs at least 300?" (ceiling) or "the most expensive one at most 300?" (floor). A hash table cannot answer these without looking at every element.
TreeSet implements NavigableSet, and TreeMap implements NavigableMap. Because the elements are sorted, the tree can answer "nearest" questions in O(log n):
| Method (set / map-key form) | Returns |
|---|---|
first() / firstKey(), last() / lastKey() |
smallest / largest |
floor(x) / floorKey(x) |
greatest element ≤ x, or null |
ceiling(x) / ceilingKey(x) |
least element ≥ x, or null |
lower(x), higher(x) |
strictly < x, strictly > x, or null |
headSet(x) / headMap(x) |
view of the elements < x (exclusive) |
tailSet(x) / tailMap(x) |
view of the elements ≥ x (inclusive) |
subSet(a, b) / subMap(a, b) |
view of a ≤ e < b |
Worked example on the set {10, 20, 30, 40}. Picture it on a number line:
10 20 30 40
5 ?----|---------|----25---|---------|----? 45
| x | floor(x) (≤) |
ceiling(x) (≥) |
lower(x) (<) |
higher(x) (>) |
|---|---|---|---|---|
| 25 | 20 | 30 | 20 | 30 |
| 30 | 30 | 30 | 20 | 40 |
| 5 | null |
10 | null |
10 |
| 45 | 40 | null |
40 | null |
The row for 30 shows the difference: floor and ceiling may return x itself; lower and higher never do. For the views: headSet(30) = [10, 20], tailSet(30) = [30, 40], subSet(15, 35) = [20, 30].
TreeMap<Integer, String> grade = new TreeMap<>();
grade.put(90, "A"); grade.put(80, "B"); grade.put(70, "C"); grade.put(0, "F");
System.out.println(grade.floorEntry(84).getValue()); // B (largest key <= 84 is 80)
This is a neat way to turn a score into a grade without a chain of ifs: 90 → A, 69 → F (largest key ≤ 69 is 0), 100 → A.
These methods live in NavigableSet/TreeSet, not in Set. A variable declared as Set<String> does not have first() — the code does not compile. Declare it as TreeSet<String> or NavigableSet<String> when you need them.
Memory trick for the views: head is exclusive, tail is inclusive — "the tail starts at x". subSet(a, b) is like substring(a, b): includes a, excludes b.
The Map API
A map is your phone's contact list: you look up a name (the key) and get a number (the value). Two contacts cannot have the same name — saving "Mum" again overwrites the old number — but two names can share the same number.
A Map<K, V> stores key → value pairs; keys are unique (they behave like a set), values may repeat.
Map<String, Integer> stock = new HashMap<>();
Integer old = stock.put("pen", 4); // null: there was no previous value
old = stock.put("pen", 7); // 4: put replaces and returns the OLD value
int pens = stock.get("pen"); // 7
int ink = stock.getOrDefault("ink", 0); // 0 instead of null
boolean has = stock.containsKey("ink"); // false
stock.remove("pen");
Step by step:
| Call | Returns | Map after | Why |
|---|---|---|---|
put("pen", 4) |
null |
{pen=4} | no old value |
put("pen", 7) |
4 | {pen=7} | same key: value replaced, old one returned |
get("pen") |
7 | {pen=7} | |
getOrDefault("ink", 0) |
0 | {pen=7} | missing key, default used |
get("ink") |
null |
{pen=7} | missing key |
remove("pen") |
7 | {} |
int n = stock.get("ink");compiles but throwsNullPointerExceptionwhen the key is missing:getreturnsnulland auto-unboxingnulltointfails. UsegetOrDefaultor checkcontainsKeyfirst.containsKeysearches the keys (fast);containsValuesearches the values, which means looking at every entry — Θ(n) even in aHashMap.
Two Java 8 methods replace most "get, test for null, put" code:
// counting: add 1, or start at 1 if the key is new
freq.merge(word, 1, Integer::sum);
// grouping: create the list the first time, then add to it
groups.computeIfAbsent(word.length(), k -> new ArrayList<>()).add(word);
In words: merge(key, 1, Integer::sum) means "if the key is new, store 1; otherwise store old + 1". computeIfAbsent(key, k -> new ArrayList<>()) means "give me the list for this key, and create an empty one first if there is none".
To walk a map, iterate over entrySet() — you get key and value together without a second lookup, and Map.Entry.setValue changes the value in place:
for (Map.Entry<String, Integer> e : freq.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
keySet() (a Set<K>), values() (a Collection<V>, may contain duplicates) and entrySet() are views: removing from them removes from the map. Adding or removing through the map while a for-each loop runs over one of its views throws ConcurrentModificationException; use iterator.remove() or map.keySet().removeIf(...) / map.entrySet().removeIf(...) instead.
A view is a window onto the map, not a copy. Think of keySet() as looking at the map through a window that shows only the names: if you rub a name off through the window, it is gone from the map too.
HashMap, LinkedHashMap, TreeMap
The three maps are the three sets again, with a value attached to each element. HashMap = fast, no order; LinkedHashMap = fast, remembers arrival order; TreeMap = a bit slower, sorted by key, can answer "nearest key" questions.
HashMap |
LinkedHashMap |
TreeMap |
|
|---|---|---|---|
| Key order when iterating | unspecified | insertion order | sorted by key |
get / put / remove |
O(1) expected | O(1) expected | O(log n) |
null key |
one allowed | one allowed | not allowed (NullPointerException) — compareTo cannot be called on null |
null values |
allowed | allowed | allowed |
| Needs from the key | equals + hashCode |
equals + hashCode |
Comparable or a Comparator |
| Extra | — | re-put of an existing key keeps its position |
firstKey, floorKey, headMap, tailMap … |
Example. Put pear=1, apple=2, fig=3, then apple=4 into a LinkedHashMap and a TreeMap:
LinkedHashMap: {pear=1, apple=4, fig=3} arrival order; apple keeps its old place, new value
TreeMap: {apple=4, fig=3, pear=1} sorted by key
The same pattern holds for sets: a HashSet is really a HashMap whose values are ignored, and a TreeSet is a TreeMap in disguise.
Typical patterns
Frequency counting — how many times does each word occur?
Map<String, Integer> freq = new TreeMap<>(); // sorted report for free
for (String w : text.split("\\s+")) freq.merge(w, 1, Integer::sum);
Trace for the text "the cat and the hat and the cat":
| Word | What merge does |
Map after the step |
|---|---|---|
| the | new → 1 | {the=1} |
| cat | new → 1 | {cat=1, the=1} |
| and | new → 1 | {and=1, cat=1, the=1} |
| the | 1 + 1 | {and=1, cat=1, the=2} |
| hat | new → 1 | {and=1, cat=1, hat=1, the=2} |
| and | 1 + 1 | {and=2, cat=1, hat=1, the=2} |
| the | 2 + 1 | {and=2, cat=1, hat=1, the=3} |
| cat | 1 + 1 | {and=2, cat=2, hat=1, the=3} |
The TreeMap keeps the keys sorted after every step, so the final report is alphabetical.
Grouping — Map<K, List<V>>, e.g. students by class, words by first letter, anagrams by their sorted letters. Grouping the words sun, moon, sky, star, cloud by length with computeIfAbsent:
| Word | List for its length | Map after the step |
|---|---|---|
| sun | 3: created, then add | {3=[sun]} |
| moon | 4: created, then add | {3=[sun], 4=[moon]} |
| sky | 3: exists, add | {3=[sun, sky], 4=[moon]} |
| star | 4: exists, add | {3=[sun, sky], 4=[moon, star]} |
| cloud | 5: created, then add | {3=[sun, sky], 4=[moon, star], 5=[cloud]} |
For anagrams, use the sorted letters as the key: "listen", "silent" and "enlist" all become "eilnst" and land in the same list.
First repetition — if (!seen.add(x)) return x; uses the boolean returned by add.
static int firstRepeat(int[] xs) {
Set<Integer> seen = new HashSet<>();
for (int x : xs) {
if (!seen.add(x)) return x; // add returned false: seen before
}
return -1;
}
For {4, 7, 2, 7, 4}: add 4 (true), add 7 (true), add 2 (true), add 7 (false) → returns 7. One pass, O(n) expected, instead of comparing every pair, Θ(n²).
Set algebra — the bulk operations modify the set they are called on, so copy first if you need the original:
Set<Integer> union = new TreeSet<>(a); union.addAll(b);
Set<Integer> inter = new TreeSet<>(a); inter.retainAll(b);
Set<Integer> diff = new TreeSet<>(a); diff.removeAll(b);
With a = {1, 2, 3, 4} and b = {3, 4, 5}:
a b
( 1 2 (3 4) 5 )
union = [1, 2, 3, 4, 5] everything in a or b
inter = [3, 4] only what is in both
diff = [1, 2] in a but not in b
a is still [1, 2, 3, 4] because we worked on copies
When a question asks "what does this print?" for a set or a map, first identify the implementation: Hash… means you cannot know the order (the question should not depend on it), LinkedHash… means insertion order, Tree… means sorted by the comparator — and check whether the comparator merges elements.
Expected complexities
A hash table jumps straight to the right bucket, like going to locker number 33 — the time does not depend on how many lockers there are. A balanced tree plays "higher or lower?": with a million elements it needs about 20 questions (log₂ 1 000 000 ≈ 20).
| Operation | HashSet / HashMap |
TreeSet / TreeMap |
|---|---|---|
add, put, contains, get, remove |
O(1) expected, O(n) worst case with many collisions | O(log n) |
| iterate over all n elements | O(n + capacity) | O(n) |
first, floor, ceiling, headSet … |
not available | O(log n) (views are created lazily) |
"Expected O(1)" assumes a reasonable hashCode that spreads keys over the buckets. A hashCode that returns a constant is legal but turns every lookup into a scan of one long bucket.
"O(1)" for a HashMap is an expected cost, not a promise for every single call. "O(log n)" for a TreeMap is a promise (the red-black tree stays balanced). If you need sorted output, sorting a HashMap's keys afterwards costs O(n log n) anyway — a TreeMap from the start is often simpler.
Key takeaways
- A set keeps each element at most once;
addreturnsfalsefor a duplicate. A map keeps each key once;putreturns the old value (ornull). - Order:
Hash…= no promised order,LinkedHash…= arrival order,Tree…= sorted. - Hash structures use
hashCodeto find the bucket andequalsinside it: override both, consistently, and do not change a key after inserting it. - Tree structures use only
compareTo/compare: a result of 0 means "same element", so a comparator that ties different objects makes the tree drop them. floor/ceilinginclude x,lower/higherexclude it;headSet(x)excludes x,tailSet(x)includes it. They exist only onTreeSet/TreeMap(orNavigable…variables).- Use
getOrDefault,merge(counting) andcomputeIfAbsent(grouping) instead of "get, test for null, put"; iterate withentrySet(). TreeMaprejectsnullkeys;HashMapandLinkedHashMapallow one.- Costs: hash = O(1) expected, tree = O(log n) guaranteed plus sorted order and nearest-element queries.
Ready? Close the notes and practise.
31 questions. Predict the output before you check — that is the skill the exam measures.