Skip to content
  • The Object Class: equals, hashCode & toString · Java

The Object Class: equals, hashCode & toString

LearnViaNotes @learnvianotes· 4 min read

Every class extends java.lang.Object. Its equals, hashCode and toString defaults are almost never what a value class wants — and equals and hashCode must be overridden together.

What Object gives every class

MethodDefault behaviourOverride?
boolean equals(Object o)Identity: this == oYes, for value classes
int hashCode()Identity-based numberAlways together with equals
String toString()ClassName@1b6d3586 (class + hex hash)Yes — for logs and debugging
Class<?> getClass()Runtime classFinal — cannot
Object clone()Shallow field copy (needs Cloneable)Rarely — prefer copy constructors
wait / notify / notifyAllMonitor-based thread coordinationFinal — cannot
finalize()Deprecated for removal (Java 9+)Never

The equals / hashCode contract

  • Reflexive: x.equals(x) is true.

  • Symmetric: x.equals(y) ⇔ y.equals(x).

  • Transitive: x.equals(y) and y.equals(z) ⇒ x.equals(z).

  • Consistent: repeated calls give the same answer while the objects don't change.

  • x.equals(null) is false.

  • Equal objects must have equal hash codes. (Unequal objects may share a hash code — that's a collision, which is legal.)

A correct implementation

Java
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Point a = new Point(1, 2), b = new Point(1, 2);
        System.out.println(a == b);          // different objects
        System.out.println(a.equals(b));     // same value
        System.out.println(a.hashCode() == b.hashCode());
        System.out.println(a);               // our toString

        Set<Point> set = new HashSet<>(List.of(a));
        System.out.println(set.contains(b));
    }
}

final class Point {
    private final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // fast path
        if (!(o instanceof Point p)) return false;     // also handles null
        return x == p.x && y == p.y;
    }

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

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

What breaks when hashCode is missing

Java
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Set<Key> set = new HashSet<>();
        set.add(new Key("id-7"));
        System.out.println(new Key("id-7").equals(new Key("id-7")));
        System.out.println(set.contains(new Key("id-7")));   // looks in the wrong bucket
    }
}

class Key {
    final String id;
    Key(String id) { this.id = id; }
    @Override public boolean equals(Object o) {
        return o instanceof Key k && id.equals(k.id);
    }
    // hashCode NOT overridden → identity hash, different for each object
}
Output
true
false

HashSet/HashMap first use hashCode() to pick a bucket and only then call equals on the entries in that bucket. Two "equal" keys with different hash codes land in different buckets and are never compared.

getClass() vs instanceof in equals

CheckSubclass instancesTrade-off
o instanceof Point pCan be equal to a PointLiskov-friendly, but symmetry breaks if a subclass adds fields and overrides equals
getClass() != o.getClass()Never equal to a PointStrictly symmetric, but a harmless subclass (or proxy) is never equal

Common guidance: make value classes final (or use a record) and use instanceof — then the question doesn't arise.

Helpers in java.util.Objects

Java
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        String a = null, b = "x";
        System.out.println(Objects.equals(a, b));          // null-safe, no NPE
        System.out.println(Objects.equals(null, null));
        System.out.println(Objects.hash(1, "a") == Objects.hash(1, "a"));
        System.out.println(Objects.hashCode(null));        // 0 for null
        System.out.println(Objects.toString(a, "n/a"));    // default for null
        System.out.println(Objects.requireNonNullElse(a, "fallback"));
        try {
            Objects.requireNonNull(a, "name must not be null");
        } catch (NullPointerException e) {
            System.out.println(e.getMessage());
        }
    }
}
Output
false
true
true
0
n/a
fallback
name must not be null

clone() — briefly

Object.clone() makes a shallow copy and only works if the class implements the marker interface Cloneable (otherwise it throws CloneNotSupportedException). It bypasses constructors and is awkward with final fields. Arrays are the one place it's pleasant: int[] copy = arr.clone();.

Why must you override hashCode when you override equals?

Can two unequal objects have the same hashCode?

What does the default toString print?

What is wrong with public boolean equals(Point other)?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report