equals, hashCode, and toString

Learning goals

Implement equals, hashCode, and toString correctly for value semantics, use Objects helpers and records, and avoid broken collections behavior from contract violations.


Why override?

Default Object.equals compares reference identity (==). Value objects (money, coordinates, users keyed by id) need logical equality.

Point a = new Point(1, 2);
Point b = new Point(1, 2);
a.equals(b);  // false unless overridden

hashCode must agree: equal objects → same hash code. Used by HashMap, HashSet.

toString aids debugging and logs.


equals contract

Reflexive, symmetric, transitive, consistent, and null-safe (return false, not NPE).

Template with Objects.equals:

public final class Point {
    private final int x;
    private final int 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 other)) return false;
        return x == other.x && y == other.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

Pattern matching instanceof Point other (JDK 16+) avoids cast.


hashCode contract

  • Equal objects → same hash.
  • Unequal objects → hashes may collide (acceptable).
  • Use Objects.hash(field1, field2, ...) or multiply-add scheme for primitives.

If you override equals, you must override hashCode—or hash collections break silently.

Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2));  // false if hashCode wrong

toString guidelines

Include class name and key fields; no sensitive data (passwords, tokens):

@Override
public String toString() {
    return "User[id=" + id + ", email=" + email + "]";
}

IDEs generate boilerplate; review output for PII in production logs.


Records (generated for free)

record Point(int x, int y) {}

Compiler generates canonical constructor, accessors, equals, hashCode, toString. Prefer records for immutable value types.

Custom behavior still allowed:

record Email(String value) {
    public Email {
        Objects.requireNonNull(value);
        if (!value.contains("@")) throw new IllegalArgumentException();
    }
}

Inheritance caution

Subclasses complicate equals. getClass() vs instanceof:

  • getClass() — strict: Point equals only Point, not subclass.
  • instanceof — Liskov-friendly for extensible hierarchies.

For entities with id:

@Override
public boolean equals(Object o) {
    if (!(o instanceof Entity other)) return false;
    return id != null && id.equals(other.id);
}

Never equate unrelated types.


Lombok / IDE generation

Tools auto-generate methods—still understand the contract for interviews and when excluding fields (transient, derived).


Common mistakes

  1. equals without hashCode — broken HashMap/HashSet.
  2. Mutable fields in equals/hashCode — object moves buckets when field changes; use immutable keys or identity.
  3. equals accepts wrong type — always check with instanceof.
  4. Including mutable or circular refs in toString — stack overflow or huge logs.
  5. Comparing floats with == in equals — use Float.compare or Double.compare.

Practice checkpoint

  1. Implement equals/hashCode for Money(long cents) value object.

    • Answer: return cents == other.cents; and Long.hashCode(cents).
  2. Two unequal objects can share hashCode—true or false?

    • Answer: True (collision allowed).
  3. Why set.contains(new Point(1,2)) fails after add if hashCode missing?

    • Answer: Set looks in wrong bucket; treats as different object.
  4. What does record give you automatically?

    • Answer: Constructor, accessors, equals, hashCode, toString.

Interview angles

  • “equals vs ==?”== identity; equals logical (overridable).
  • “hashCode contract?” — Consistent with equals; used for bucket indexing.
  • “Can equal objects have different hash codes?” — No (contract violation if equal and different hash).

Next: /java/learn/oop/enums-and-nested-types/