Maps, Sets and equals/hashCode

Java · Practical · 14 min

Lookups by key and de-duplication are everywhere, and broken equals/hashCode is one of the most common subtle bugs.

A Map associates keys with values; each key appears at most once. HashMap is the default: average constant-time put and get, no ordering. LinkedHashMap keeps insertion order, and TreeMap keeps keys sorted.

Idiomatic map code uses the newer methods instead of check-then-act sequences:

  • getOrDefault(key, fallback) — avoid nulls for missing keys.
  • merge(word, 1, Integer::sum) — counting in one line.
  • computeIfAbsent(key, k -> new ArrayList<>()).add(value) — grouping into lists.
  • Iterate with for (var e : map.entrySet()) and use e.getKey(), e.getValue().

A Set holds unique elements. HashSet is the default, LinkedHashSet preserves insertion order, TreeSet keeps elements sorted. set.add(x) returns false if x was already present.

Both hash-based structures depend on two methods every object inherits from Object. The contract: if a.equals(b) then a.hashCode() == b.hashCode(). The hash code picks a bucket, equals confirms the match. If you override equals but not hashCode, two "equal" keys land in different buckets and lookups mysteriously fail.

The easiest way to get both right is to use a record or String as a key. For a regular class, generate them with your IDE or use Objects.equals and Objects.hash.

Never mutate an object in a way that changes its hash code while it is a key in a map or member of a set — it becomes unreachable. Immutable keys avoid this entirely.

Map.of(...) and Set.of(...) create small unmodifiable collections, and Set.of rejects duplicate elements.

Example

import java.util.*;

public class MapsSets {
    record Coord(int x, int y) { }                    // correct equals/hashCode for free

    public static void main(String[] args) {
        String text = "the cat and the hat and the bat";
        Map<String, Integer> counts = new TreeMap<>();
        for (String w : text.split(" ")) counts.merge(w, 1, Integer::sum);
        System.out.println(counts);   // {and=2, bat=1, cat=1, hat=1, the=3}

        Map<Integer, List<String>> byLength = new HashMap<>();
        for (String w : counts.keySet()) {
            byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w);
        }
        System.out.println(byLength.get(3));          // [and, bat, cat, hat, the]

        Set<Coord> visited = new HashSet<>();
        visited.add(new Coord(1, 2));
        System.out.println(visited.add(new Coord(1, 2)));  // false: already present
        System.out.println(counts.getOrDefault("dog", 0)); // 0
    }
}

Remember

  • HashMap/HashSet are the defaults; Linked* keep insertion order, Tree* keep sorted order
  • Use merge, computeIfAbsent and getOrDefault instead of check-then-put
  • Equal objects must have equal hash codes — records do this for you

Watch out for

  • Overriding equals without hashCode, so equal keys aren't found in a HashMap.
  • Mutating a key after inserting it into a HashMap or HashSet.

Check yourself

  1. A class overrides equals but not hashCode. What typically happens with `set.contains(equalCopy)` on a HashSet?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/tutorial/collections/interfaces/map.html

Maps, Sets and equals/hashCode · Java · Topolearn