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
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)); } }
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
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)); } }
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
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))); } }
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
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)); } }
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
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()); } }
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
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)); } }
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.
true [ISBN 9781234567897]