Why does every class in Java extend Object, and what methods does that give you for free?
Every class in Java extends java.lang.Object, whether you write Because extends Object or not. It is the root of the type hierarchy, and it exists so that every object — no matter how simple — has a common set of operations and a common runtime type.java.lang is imported automatically, Object is always in scope.
The methods it supplies fall into four groups. Identity and type: getClass returns the runtime class, and it is final so nobody can lie about it. Equality: equals and hashCode define how an object compares and how it is bucketed in a hash-based collection; the default implementations use identity. Rendering: toString returns a description of the object, and its default form is the class name and the identity hash code in hex. Lifecycle: clone and finalize, the latter of which is deprecated for removal because it is unreliable.
The methods it supplies fall into four groups. Identity and type: the getClass method returns the runtime class, and it is final so nobody can lie about it. Equality: equals and hashCode define how an object compares and how it is bucketed in a hash-based collection, and the default implementations use identity. Rendering: toString returns a description of the object, and its default form is the class name followed by the identity hash code in hex. Lifecycle: clone and finalize, the latter of which is deprecated for removal because it is unreliable.
Two practical points follow from this. Because equals defaults to identity, a class that represents a value rather than a thing — a domain object, a record, a data transfer object — must override equals and hashCode together, or it will behave wrongly in a HashMap. And because the default toString prints an identity hash, logging an object without overriding it gives you output that changes between runs and tells you nothing, which is why overriding toString is one of the first things to do with a new class.
One more subtlety: getClass is final, but equals is not. That is deliberate — an equals that checks getClass cannot be extended symmetrically, whereas one that uses an instanceof test and a canEqual-style hook can.
public class Main {
public static void main(String[] args) {
// A String is an Object too.
Object anything = "a string is an Object too";
System.out.println("toString : " + anything.toString());
System.out.println("hashCode : " + anything.hashCode());
System.out.println("equals self: " + anything.equals(anything));
System.out.println("getClass : " + anything.getClass().getName());
// The default toString is rarely what you want, so classes override it.
System.out.println("default : " + new Point(1, 2));
}
}
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}Two practical points follow from this. Because equals defaults to identity, a class that represents a value rather than a thing — a domain object, a record, a DTO — must override equals and hashCode together, or it will behave wrongly in a HashMap. And because the default toString prints an identity hash, logging an object without overriding it gives you output that changes between runs and tells you nothing, which is why overriding toString is one of the first things to do with a new class.
One more subtlety: getClass is final, but equals is not. That is deliberate — an equals that checks getClass() cannot be extended symmetrically, whereas one that uses instanceof and a canEqual-style hook can.