skip to content

When you use a String as a key in a HashMap, what role do equals() and hashCode() play, and why would using == instead break lookups?

level: seniorimportance: should knowfreq 55%

answer

  1. HashMap: hashCode → bucket, equals → match in bucket
  2. Contract: equal objects ⇒ equal hashCode
  3. String overrides both, content-based + consistent
  4. == match would fail for runtime-built keys
  5. Override equals ⇒ override hashCode (Objects.hash)

basics

~20 s

HashMap finds a key by its hashCode() to pick a bucket, then uses equals() to match within that bucket. String overrides both consistently, so lookups work by content. If maps used == they would fail whenever a key was rebuilt at runtime instead of being the same object.

solid answer

~50 s

A HashMap stores entries in buckets chosen by the key's hashCode(). On get/put it computes the key's hash, jumps to the bucket, then compares candidate keys with equals() to find the exact match. So both methods matter, and they must obey the equals/hashCode contract: equal objects must have equal hash codes. String overrides hashCode() to be a content-based value (a deterministic formula over its characters) and equals() to compare content, and the two are consistent, so a lookup key built at runtime with the same characters as the stored key resolves correctly. If maps compared keys with == (reference identity), then a key like new String("id") or a runtime-concatenated key would land in the right bucket by hash but fail the identity check, so get() would return null even though the text matches. That is exactly why == must never be used for content equality, and why a correct equals() implementation always pairs with a consistent hashCode().

code

java · 12 lines
java
Map<String, Integer> ages = new HashMap<>();
ages.put("alice", 30);

String stored = "alice";
String rebuilt = new String("alice"); // different object

System.out.println(stored == rebuilt);            // false (identity differs)
System.out.println(stored.equals(rebuilt));       // true  (content matches)
System.out.println(stored.hashCode() == rebuilt.hashCode()); // true (content-based)
System.out.println(ages.get(rebuilt));            // 30 — found via hashCode + equals

// If the map matched with ==, ages.get(rebuilt) would be null.

go deeper

for a junior

Knows a HashMap looks up String keys by their content, so equals() (not ==) is what matters for lookups.

for a middle

Explains the hashCode-then-equals lookup steps and that String overrides both consistently.

for a senior

States the full equals/hashCode contract, explains why == would break lookups for runtime-built keys, and always pairs equals/hashCode overrides; understands immutability's role.

for a principal

Reasons about collision behavior and treeification, the cost of poor hash distribution, immutability guarantees for keys, and codifies equals/hashCode/Comparable consistency conventions across the codebase.

## What a HashMap does, step by step A `HashMap<K, V>` is a lookup table. Internally it has an array of **buckets**. To store or find an entry it does two things: 1. **Hash to a bucket.** It calls `key.hashCode()` (then spreads/mixes the bits and takes it modulo the array size) to choose which bucket index to use. This is O(1) and narrows the search to one small bucket. 2. **Match within the bucket.** A bucket may hold several keys whose hashes collided. So the map walks the bucket and uses `key.equals(candidate)` to find the *exact* key. The value of the matching entry is returned. So a successful lookup requires **both**: the right bucket (hashCode) *and* an equals() match inside it. ## The equals/hashCode contract Because of that two-step process, Java mandates a contract between the two methods: - If `a.equals(b)` is `true`, then `a.hashCode() == b.hashCode()` **must** be true. - Equal hash codes do *not* require equality (collisions are allowed) — that just puts unequal keys in the same bucket, resolved by equals(). - `hashCode()` must be consistent: the same object returns the same value as long as the fields used by equals() don't change. If you break this (e.g. override equals() but not hashCode()), two 'equal' keys can hash to different buckets, and a lookup with one will never find the entry stored under the other — a silent, severe bug. ## Why String works perfectly as a key `String` overrides **both** methods consistently: - `String.equals()` compares content (characters). - `String.hashCode()` computes a content-based integer via a fixed formula (`s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]`), cached after first computation. Two strings with the same characters always produce the same hash. Because equal content ⇒ equal hash, a key you build at runtime resolves to the same bucket and matches via equals(): ```java Map<String, Integer> ages = new HashMap<>(); ages.put("alice", 30); String lookup = new String("alice"); // different object than the stored key ages.get(lookup); // 30 — correct, content-based System.out.println("ali" + "ce".equals(lookup)); // illustrates content match ``` Even though `lookup` is a *different object* (`lookup == storedKey` would be false), the map finds the entry because it uses hashCode()+equals(), both content-based. ## Why == would break it Imagine a (hypothetical, broken) map that matched keys with `==` instead of `equals()`. The hash step would still place the lookup key in the correct bucket (hash is content-based), but the in-bucket comparison would demand the *same object*. A key created via `new String(...)`, concatenation, parsing, or deserialization is a different object, so `==` would be false and `get()` would return `null` despite identical text. Your data would seem to vanish whenever the key wasn't the literal you originally stored. This is the concrete, costly consequence of confusing reference equality with content equality — the same mistake as using `==` to compare strings directly. ## Practical implications - Always use `equals()` for content; never `==`. - Whenever you override `equals()` in your own class, override `hashCode()` to stay consistent (use `Objects.hash(...)` / `Objects.equals(...)`). - Don't mutate fields used in equals()/hashCode() while an object is a key in a hash structure — the entry becomes unreachable. - `String` is an excellent key precisely because it is immutable (its hash never changes) and its equals/hashCode are content-based and consistent.

  • What happens if you override equals() but forget hashCode()?
    Two objects that are equals() can return different hash codes, so a HashMap/HashSet may place them in different buckets. A lookup with an equal-but-different object then returns null, and a HashSet may store apparent duplicates. Always override both together (e.g. with Objects.hash).
  • Why is String's immutability important for using it as a map key?
    Because its content (and thus its hashCode and equals result) can never change after creation, an entry can never become unreachable due to key mutation. Mutable keys whose equals/hashCode fields change while in a map cause lost entries.

Finding a key in a HashMap is like finding a book in a library: hashCode() is the shelf number (gets you to the right shelf fast), and equals() is reading the titles on that shelf to pick the exact book. If you demanded the same physical copy you once placed (==), an identical reprint on the shelf would never count as a match.

saying these in an interview costs you the question

  • Thinking HashMap uses == to compare keys
  • Overriding equals() without hashCode()
  • Believing equal hashCodes mean the objects are equal (collisions exist)
  • Using a mutable object as a hash key and mutating its equals-relevant fields
  • Assuming get() with a new String key fails (it succeeds, because lookups are content-based)

context