🔑 Section 10 · Question #7

What is the equals() and hashCode() Contract

The equals() and hashCode() contract is a foundational set of rules defined in the Java Language Specification (java.lang.Object) that governs the relationship between logical o...


🟢 Junior Level

Short Interview Answer (30 Seconds)

The equals() and hashCode() contract is a foundational set of rules defined in the Java Language Specification (java.lang.Object) that governs the relationship between logical object equality and numeric hash codes:

  1. Mandatory Rule: If two objects are logically equal according to equals() (x.equals(y) == true), their hashCode() methods MUST return identical integer values (x.hashCode() == y.hashCode()).
  2. Non-Mandatory Reverse: If two objects share the exact same hashCode(), they are NOT necessarily equal according to equals() (this is a hash collision).
  3. Consistency: Multiple invocations of hashCode() and equals() on the same object must return identical results, provided no fields evaluated in the comparison have been modified.

If this contract is violated (for instance, overriding equals() while neglecting hashCode()), hash-based collections (HashMap, HashSet, ConcurrentHashMap) break: previously stored objects cannot be retrieved via get(), and HashSet will silently store duplicate elements.


The 5 Formal Axioms of equals() (Java SE Specification)

For any non-null object references x, y, and z, equals() must satisfy the mathematical properties of an equivalence relation:

Property Axiom Rule Description
1. Reflexive x.equals(x) == true An object must always be equal to itself.
2. Symmetric x.equals(y) == y.equals(x) If $X$ equals $Y$, then $Y$ must equal $X$.
3. Transitive If x.equals(y) and y.equals(z), then x.equals(z) If $X = Y$ and $Y = Z$, then $X$ must equal $Z$.
4. Consistent Repeated calls yield identical results Unchanged object state must never alter equality results.
5. Non-Nullity x.equals(null) == false Comparing against null must return false without throwing NullPointerException.

Idiomatic Implementation in Java 21

import java.util.Objects;

// 1. Modern approach: Java Records automatically generate a contract-compliant implementation
public record UserRecord(long id, String email) {}

// 2. Canonical POJO class with explicit overrides
public class UserProfile {
    private final long id;
    private final String email;

    public UserProfile(long id, String email) {
        this.id = id;
        this.email = email;
    }

    @Override
    public boolean equals(Object o) {
        // Step 1: Reference identity check (fastest)
        if (this == o) return true;
        // Step 2: Null and exact class type check (avoids inheritance symmetry issues)
        if (o == null || getClass() != o.getClass()) return false;
        // Step 3: Type cast and field-by-field equality evaluation
        UserProfile that = (UserProfile) o;
        return this.id == that.id && Objects.equals(this.email, that.email);
    }

    @Override
    public int hashCode() {
        // Hash computation based on the EXACT same fields used in equals()!
        return Objects.hash(id, email);
    }
}

🟡 Middle Level

Joshua Bloch’s Recipe and the Magic of 31

In Effective Java, Joshua Bloch established the standard recipe for computing hash codes:

@Override
public int hashCode() {
    int result = 17; // Non-zero prime seed
    result = 31 * result + (int) (id ^ (id >>> 32)); // Fold 64-bit long
    result = 31 * result + (email != null ? email.hashCode() : 0);
    return result;
}

Why the Prime Multiplier 31:

  1. Odd Prime Number: Multiplying by an odd prime minimizes clustering across hash buckets. Using an even multiplier (like 2) is equivalent to a left shift, shifting high bits out of the 32-bit int boundary and losing information.
  2. JIT Compiler Micro-Optimization:
    The HotSpot JIT compiler replaces multiplication by 31 with an instruction-level shift and subtraction: \(31 \times i = (i \ll 5) - i\) This evaluates in 1 CPU clock cycle, bypassing the hardware integer multiplication unit (imul).

Symmetry Violation in Inheritance (instanceof vs. getClass)

A subtle bug frequently occurs when subclassing classes that override equals() using instanceof:

public 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 (!(o instanceof Point p)) return false;
        return this.x == p.x && this.y == p.y;
    }
}

public class ColorPoint extends Point {
    private final String color;
    public ColorPoint(int x, int y, String color) { super(x, y); this.color = color; }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint cp)) return false;
        return super.equals(o) && Objects.equals(this.color, cp.color);
    }
}

The Contract Collapse:

Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, "RED");

p.equals(cp);  // true  (Point checks x, y; ColorPoint IS an instanceof Point)
cp.equals(p);  // false (ColorPoint requires instanceof ColorPoint; Point is NOT!)

Symmetry is completely broken! Adding both objects to a collection yields non-deterministic behavior depending on insertion order.

[!TIP] Effective Java Architectural Rule:
It is mathematically impossible to extend an instantiable class and add a new value component while preserving the equals() contract. Prefer Composition over Inheritance (ColorPoint contains a Point field), or enforce exact type matching via getClass() != o.getClass().


🔴 Senior Level

HotSpot Memory Architecture: The Mark Word identityHashCode

When a class does not override hashCode(), calls dispatch to native Object.hashCode(), returning the Identity Hash Code.

64-bit Object Header (Mark Word in 64-bit HotSpot JVM):
┌──────────────────────────────────────────────┬────────┬────────┬───────┐
│              Identity Hash Code (31 bits)     │ Unused │ Age (4)│ Biased│ ... Lock Bits (2)
└──────────────────────────────────────────────┴────────┴────────┴───────┘
  1. Lazy Generation: An object’s identityHashCode is not computed at instantiation. Upon the first call to System.identityHashCode(obj), HotSpot generates a pseudorandom number (via Marsaglia XOR-shift) and permanently writes it into the Mark Word of the object header in RAM.
  2. Immutability Across GC Relocations: When the garbage collector relocates an object between generations or compacts the heap, the physical memory address changes, but the identity hash code in the header remains untouched.
  3. Displaced Mark Word Under Heavy Synchronization (Fat Lock): If an object is locked via synchronized(obj) and inflates to a kernel-level ObjectMonitor, HotSpot replaces the object header with a native pointer to the C++ monitor. To maintain the contract, HotSpot saves the original hash in ObjectMonitor._header (Displaced Mark Word).

Dangerous Production Anti-Patterns: java.net.URL and JPA Entities

1. The java.net.URL.equals() Network Catastrophe

The java.net.URL class contains one of Java’s oldest architectural flaws: URL.equals() attempts to resolve hostnames to IP addresses via blocking synchronous DNS queries!

  • Calling urlMap.get(url) fires a network call to a DNS server;
  • If the DNS server is unresponsive, the calling thread freezes for seconds;
  • If a domain uses Round-Robin DNS, subsequent calls to equals() on identical URLs return different IPs, violating the consistency axiom.
    Production Solution: Always use java.net.URI, which performs strict string-based character comparison without opening network sockets.

2. JPA / Hibernate Entity with @GeneratedValue ID

Junior engineers often generate equals and hashCode using an autoincrement database ID:

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id; // null prior to entityManager.persist()!
}
  1. Instantiate Order order = new Order(). The id is null.
  2. Add to a HashSet<Order>: hash code is computed from null.
  3. Persist entity: database assigns id = 42.
  4. The entity’s hash code has mutated while stored inside the HashSet!
  5. Subsequent calls to set.contains(order) or set.remove(order) return false. The entry is orphaned in memory, causing a classic Memory Leak.
    Production Solution: Use an immutable Natural Business Key (UUID, email) or implement a fixed constant hash code pattern (return 31;) combined with null-safe ID checks in equals().

🎯 Interview Cheat Sheet

1. 30-Second Elevator Pitch

The equals() and hashCode() contract mandates that if two objects are equal by equals(), their hashCode() results MUST be identical. The reverse is not required (identical hashes indicate a collision). equals() must be reflexive, symmetric, transitive, consistent, and null-safe. Violating the contract breaks hash collections (HashMap, HashSet), causing lost lookups and duplicate set entries. In modern Java, use record or getClass() checks rather than instanceof to avoid symmetry bugs in class hierarchies.


2. 4 Tricky Questions with Deep Answers

Question 1: Why is it mathematically impossible to add a new value component to an instantiable class via inheritance while preserving the equals() contract?

Answer: If the superclass uses instanceof and ignores the subclass field, transitivity breaks: ColorPoint(1, 2, RED).equals(Point(1, 2)) is true, and Point(1, 2).equals(ColorPoint(1, 2, BLUE)) is true, but ColorPoint(1, 2, RED).equals(ColorPoint(1, 2, BLUE)) evaluates to false ($A=B$ and $B=C$, but $A \ne C$).
If the subclass enforces its field against the superclass, symmetry breaks: point.equals(colorPoint) is true, but colorPoint.equals(point) is false.
The only architectural solution is Composition over Inheritance: make ColorPoint contain a Point as an internal member.

Question 2: How is identityHashCode stored in HotSpot memory, and why does it not change when the GC moves the object in heap?

Answer: The default identityHashCode is lazily generated and stored in a 31-bit field of the object’s 64-bit Mark Word header. When a copying or compacting garbage collector (G1, ZGC, Parallel) evacuates an object to a new heap memory address, it copies the entire object header intact, preserving the original 31-bit value. Even if lock inflation replaces the header with an ObjectMonitor pointer, HotSpot preserves the displaced mark word inside the monitor structure.

Question 3: Why is using java.net.URL as a key in a HashMap a critical production hazard?

Answer: java.net.URL.equals() and URL.hashCode() perform synchronous, blocking network DNS lookups to compare the IP addresses of the hostnames. If the DNS server is slow or network connectivity degrades, basic map.get() calls block the thread for seconds. Furthermore, round-robin DNS or dynamic IP reassignment changes the IP over time, mutating the hash code and violating the consistency axiom. Use java.net.URI instead.

Question 4: How should equals() and hashCode() be implemented for JPA / Hibernate entities with database-generated IDs?

Answer: Autoincrement @Id fields start as null before persist() and mutate to a number upon flush, breaking HashSet bucket locations.
Two production patterns solve this:

  1. Natural Business Key: Compare unique, immutable domain attributes (UUID, username, tax ID) set at construction.
  2. Fixed Hash Code Pattern: Return a constant hash code (e.g. return getClass().hashCode();), and compare IDs in equals() only if both are non-null (id != null && id.equals(other.id)). This forces pre-persisted entities into the same bucket temporarily, but guarantees that the hash code never mutates after database persistence.

3. Common Pitfalls and Red Flags

❌ “If two objects have the same hashCode(), they must be equal by equals().”
Correction: False. This confuses consequence with cause. Equal hash codes simply mean a hash collision occurred.

❌ “Overloading equals() instead of overriding it: public boolean equals(MyClass o).”
Correction: This creates an overloaded method. Collections call equals(Object o), meaning your custom method will be ignored and Object.equals will run.

❌ “Using random numbers or timestamps inside hashCode().”
Correction: This violates the consistency axiom. The hash must be calculated strictly from deterministic object state.