The Object Class: equals, hashCode & toString

Every Java class ultimately extends java.lang.Object, inheriting a handful of methods. Three of them — toString, equals and hashCode — have defaults that are rarely what you want for your own classes. Overriding them correctly makes objects printable, comparable by content, and usable in sets and maps.

toString

JAVA
class Book {
    final String title;
    final int year;
    Book(String title, int year) { this.title = title; this.year = year; }
}

class BetterBook {
    final String title;
    final int year;
    BetterBook(String title, int year) { this.title = title; this.year = year; }

    @Override
    public String toString() {
        return title + " (" + year + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new Book("Small Rivers", 2021).toString().startsWith("Book@"));
        System.out.println(new BetterBook("Small Rivers", 2021));
    }
}
Output
true
Small Rivers (2021)

The default toString returns the class name, @ and a hash code — something like Book@3b9a45b3. Overriding it makes printing, logging and debugging far more useful. println and string concatenation call it automatically.

equals: identity vs equality

JAVA
import java.util.Objects;

class Point {
    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;
        if (!(o instanceof Point other)) return false;
        return x == other.x && y == other.y;
    }

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

public class Main {
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println(a == b);
        System.out.println(a.equals(b));
        System.out.println(a.equals(null));
    }
}
Output
false
true
false

The default equals is the same as ==: two objects are equal only if they're the same object. Overriding it defines logical equality. A correct equals must be reflexive, symmetric and transitive, and return false for null — the pattern above handles all of that.

hashCode must agree with equals

JAVA
import java.util.HashSet;
import java.util.Set;

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

    @Override
    public boolean equals(Object o) {
        return o instanceof BadPoint p && p.x == x && p.y == y;
    }
}

public class Main {
    public static void main(String[] args) {
        Set<BadPoint> set = new HashSet<>();
        set.add(new BadPoint(1, 2));
        System.out.println(set.contains(new BadPoint(1, 2)));
    }
}
Output
false

Hash-based collections (HashSet, HashMap) first use hashCode to find a bucket, then equals within it. If equal objects have different hash codes, the set looks in the wrong bucket and "loses" the object. Rule: whenever you override equals, override hashCode using the same fields. Objects.hash(…) makes that easy.

Records do it for you

JAVA
import java.util.Set;

record Point(int x, int y) {}

public class Main {
    public static void main(String[] args) {
        Set<Point> visited = Set.of(new Point(0, 0), new Point(1, 2));
        System.out.println(visited.contains(new Point(1, 2)));
        System.out.println(new Point(1, 2));
    }
}
Output
true
Point[x=1, y=2]

For data classes, records generate correct equals, hashCode and toString automatically — one of their biggest advantages. IDEs can also generate these methods for ordinary classes.

getClass and other Object methods

JAVA
public class Main {
    public static void main(String[] args) {
        Object o = "hello";
        System.out.println(o.getClass().getName());
        System.out.println(o.getClass().getSimpleName());
        System.out.println(Integer.valueOf(5).getClass().getSuperclass().getSimpleName());
    }
}
Output
java.lang.String
String
Number

Object also provides getClass() (runtime type information), wait/notify (low-level thread coordination, rarely used directly today) and clone. Because everything is an Object, a variable of type Object can hold any value — though you lose type safety, which is why generics (lesson 39) exist.

The java.util.Objects helpers

JAVA
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        String a = null;
        System.out.println(Objects.equals(a, null));
        System.out.println(Objects.toString(a, "(none)"));
        System.out.println(Objects.requireNonNullElse(a, "default"));
        System.out.println(Objects.hash("x", 1) == Objects.hash("x", 1));
    }
}
Output
true
(none)
default
true

Objects offers null-safe versions of common operations, and Objects.requireNonNull(arg, "message") is the standard way to reject null arguments early.

Try it yourself

Give an Isbn class value-based equality and check it works in a set.

Try it yourself · Main.java
Edit the code, copy it into Main.java and run java Main.java (Java 25+). How to set up Java
Output
true
[ISBN 9781234567897]