Object Class

Every class in Java, whether it says so or not, extends Object — and inherits a handful of methods (toString, equals, hashCode, getClass, wait/notify) that quietly shape how every object prints, compares, and participates in concurrent code.

Object sits at the root of every Java class hierarchy. Understanding the small set of methods it defines — and their default behavior versus what a well-designed class should override — is foundational to writing correct Java classes.

Every Class Extends Object

If a class declaration doesn't say extends SomeClass, the compiler implicitly makes it extend java.lang.Object. This means every Java object — regardless of its class — carries the same baseline set of methods: toString(), equals(Object), hashCode(), getClass(), clone(), wait()/notify()/notifyAll(), and the deprecated/removed finalize().

class Point { int x, y; }
// Implicitly: class Point extends Object { int x, y; }

toString()

toString() returns a String representation of the object; it is called automatically by string concatenation, System.out.println(obj), and String.valueOf(obj). Object's default implementation returns the class name, an @ sign, and the object's hash code in hexadecimal — informative for debugging, but rarely what you want to show a user.

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

public class Main {
    public static void main(String[] args) {
        Point p = new Point(3, 4);
        System.out.println(p); // e.g. Point@1b6d3586 (default Object.toString())
    }
}
class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}
// System.out.println(p) now prints: Point(3, 4)

equals(Object)

Object's default equals(Object) implementation compares reference identity — it behaves exactly like ==, returning true only when both references point at the very same object. Any class that wants "same content" equality (two different Point objects both at (3,4) being considered equal) must override equals() itself.

equals() behavior depends entirely on how a class implements it. Not every class's equals() performs deep or value-based comparison — the default from Object always compares identity unless overridden.

class Point {
    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 this.x == other.x && this.y == other.y;
    }
}

hashCode()

hashCode() returns an int used by hash-based collections (HashMap, HashSet) to bucket objects efficiently. Object's default implementation typically derives a value related to the object's identity (implementation-dependent — do not assume it reflects memory address in a portable way). Any class that overrides equals() must also override hashCode() so that equal objects always produce the same hash code — this is a strict contractual requirement, covered in depth in the Equality & Hashing topic.

class Point {
    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 java.util.Objects.hash(x, y);
    }
}

getClass()

getClass() returns the runtime Class object representing the object's actual class — useful for reflection, logging, and precise type checks. Unlike instanceof, which returns true for subtypes too, getClass() comparisons (a.getClass() == b.getClass()) require the exact same runtime class, which some equals() implementations deliberately use for stricter equality.

Object o = "hello";
System.out.println(o.getClass());        // class java.lang.String
System.out.println(o.getClass().getSimpleName()); // String

clone() — Historical Caveats

Object declares a protected clone() method intended to create and return a copy of the object. It is widely considered a design misstep in the JDK: it requires implementing the empty marker interface Cloneable, throws the checked CloneNotSupportedException if that interface is absent, performs only a shallow field-by-field copy by default, and interacts awkwardly with final fields and constructors (clone() does not call any constructor at all).

Modern Java code generally avoids Object.clone() in favor of copy constructors, static factory "copyOf" methods, or (since records exist) simply building a new immutable instance. It is documented here as a historical, still-present mechanism, not a recommended pattern.

class Counter implements Cloneable {
    int count;
    @Override
    public Counter clone() {
        try {
            return (Counter) super.clone(); // shallow copy
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e); // cannot happen: we implement Cloneable
        }
    }
}

wait(), notify(), notifyAll()

Object also defines the low-level thread-coordination methods wait(), notify(), and notifyAll(). Every object carries an intrinsic monitor lock, and these methods let a thread holding that lock release it and wait to be signaled, or wake up one (notify) or all (notifyAll) other threads waiting on the same object. They must be called from within a synchronized block or method on that object, or they throw IllegalMonitorStateException.

In modern Java, higher-level concurrency utilities (java.util.concurrent — locks, executors, synchronizers) are generally preferred over calling wait/notify directly; they are covered in the Concurrency group.

class SharedBuffer {
    private boolean ready = false;

    synchronized void produce() {
        ready = true;
        notifyAll(); // wake any threads waiting on this object's monitor
    }

    synchronized void consume() throws InterruptedException {
        while (!ready) {
            wait(); // releases the monitor and waits to be notified
        }
        System.out.println("Consumed");
    }
}

finalize() — Deprecated

Object historically declared finalize(), invoked by the garbage collector (at an unspecified, unreliable time, if ever) before reclaiming an object. It has been deprecated since Java 9 and is unreliable for resource cleanup: GC timing is non-deterministic, finalize() can be skipped entirely, and it can even resurrect objects. Modern code uses try-with-resources and AutoCloseable instead; finalize() is covered fully, as a historical mechanism, in a dedicated topic.

The equals/hashCode Contract, Summarized

Object's documentation establishes a strict contract that any override must respect: equals() must be reflexive, symmetric, transitive, and consistent, and — critically — two objects that are equal via equals() must return the same hashCode(). Violating this contract silently breaks hash-based collections (an object can seem to "disappear" from a HashSet). Full treatment is in the Equality & Hashing topic.

Which Methods You Typically Override

In everyday application code, toString(), equals(), and hashCode() are the three Object methods routinely overridden. getClass() is used but never overridden (it cannot be — it is final). clone() is overridden only when a class specifically needs the legacy cloning protocol. wait/notify/notifyAll are called, not overridden, and mostly in low-level concurrency code. finalize() should not be used in new code at all.

Practical Usage: IDE and Record Generation

Because writing correct toString(), equals(), and hashCode() by hand is repetitive and error-prone, most IDEs can generate them automatically from a class's fields, and java.util.Objects provides equals() and hash() helper methods to reduce boilerplate. Records (Java 16+) generate all three automatically based on their components, which is one of their major appeals for simple data carriers.

record Point(int x, int y) { }
// toString(), equals(), and hashCode() are all generated automatically,
// based on the record's components (x and y).

Edge Cases

Calling wait() or notify() on an object outside a synchronized block on that same object throws IllegalMonitorStateException at runtime, not at compile time.

Overriding equals() without also overriding hashCode() compiles fine but breaks HashMap/HashSet behavior silently — no compiler warning is guaranteed for every such case, so it is a well-known source of subtle bugs.

getClass() cannot be overridden — it is declared final in Object — so it always reflects the true runtime class regardless of any static type.

Best Practices

Override toString() for any class whose instances are likely to be logged, printed, or debugged. Override equals() and hashCode() together, never just one, whenever value-based (rather than identity-based) equality is meaningful for the class. Avoid Object.clone() and finalize() in new code; prefer copy constructors/factory methods and try-with-resources/AutoCloseable respectively.

Complete Example

import java.util.Objects;

class Point {
    private final int x, y;

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

    @Override
    public String toString() {
        return "Point(" + x + ", " + 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);              // Point(1, 2)
        System.out.println(a.equals(b));     // true
        System.out.println(a.hashCode() == b.hashCode()); // true
        System.out.println(a.getClass());    // class Point
    }
}

Key Takeaways

Every class implicitly extends Object and inherits toString(), equals(), hashCode(), getClass(), clone(), wait()/notify()/notifyAll(), and finalize().

Object's default equals() compares reference identity; override it for value-based comparison.

equals() and hashCode() must always be overridden together to satisfy their required contract.

clone() and finalize() are legacy mechanisms with well-known problems; prefer copy constructors and try-with-resources/AutoCloseable in modern code.

wait()/notify()/notifyAll() operate on an object's intrinsic monitor and require holding that monitor's lock via synchronized.

getClass() is final and always returns the true runtime type of the object.

class Point { int x, y; @Override public boolean equals(Object o) { if (!(o instanceof Point p)) return false; return x == p.x && y == p.y; } // hashCode() not overridden -- still uses Object's identity-based default } class Point { int x, y; @Override public boolean equals(Object o) { if (!(o instanceof Point p)) return false; return x == p.x && y == p.y; } @Override public int hashCode() { return java.util.Objects.hash(x, y); } }

Overriding equals() without hashCode() breaks the equals/hashCode contract: two "equal" Points can end up with different hash codes, causing them to be treated as distinct in a HashSet or as HashMap keys.

class Data implements Cloneable { int[] values; @Override public Data clone() { try { return (Data) super.clone(); // shallow copy -- values array is shared! } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } } class Data { private final int[] values; Data(int[] values) { this.values = values.clone(); } // defensive copy in constructor Data copy() { return new Data(this.values); } // explicit copy method }

Object.clone()'s default field-by-field copy is shallow: array and object fields are shared between the original and the clone, not duplicated. A copy constructor or factory method makes the copying strategy explicit and easier to get right.

synchronized (this) { //... } obj.wait(); // called outside any synchronized block on obj -- throws IllegalMonitorStateException synchronized (obj) { obj.wait(); } wait() (and notify()/notifyAll()) must be called while the calling thread holds the monitor lock of the exact object they are invoked on; otherwise the JVM throws IllegalMonitorStateException at runtime.

If a class declares no extends clause, what does it implicitly extend?

java.lang.Object. Every class in Java ultimately descends from Object, whether stated explicitly or not.

What does Object's default equals(Object) implementation actually compare?

Reference identity — it returns true only if both references refer to the exact same object in memory, behaving identically to the == operator on references, unless the class overrides equals().

Why must hashCode() be overridden whenever equals() is overridden?

Because the Object contract requires that any two objects considered equal by equals() must return the same hashCode(). Breaking this silently corrupts behavior in HashMap, HashSet, and other hash-based collections.

Name two problems with Object.clone() that lead most modern code to avoid it.

It performs only a shallow copy by default (shared array/object fields between original and clone), and it requires implementing the marker interface Cloneable plus catching the checked CloneNotSupportedException — awkward compared to a simple copy constructor or factory method.

Why is finalize() considered unreliable for resource cleanup, and what should be used instead?

Garbage collection timing is non-deterministic and not guaranteed to run at all before JVM shutdown, so finalize() may run late or never; it has been deprecated since Java 9. try-with-resources with AutoCloseable provides deterministic, explicit cleanup instead.

What exception is thrown if wait() is called on an object without holding that object's monitor lock?

IllegalMonitorStateException, thrown at runtime.

Interview-style: why does Object provide wait/notify/notifyAll rather than putting them on a separate concurrency-specific type?

Because every object in Java automatically has an intrinsic monitor lock (used by the synchronized keyword), Java's original design attached the coordination primitives that operate on that monitor directly to Object, so any object can be used for low-level thread signaling without extra scaffolding.

Write a class Money with fields amount (long, cents) and currency (String) that overrides toString(), equals(), and hashCode() correctly.

import java.util.Objects; class Money { private final long amountCents; private final String currency; Money(long amountCents, String currency) { this.amountCents = amountCents; this.currency = currency; } @Override public String toString() { return amountCents + " " + currency; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Money other)) return false; return amountCents == other.amountCents && currency.equals(other.currency); } @Override public int hashCode() { return Objects.hash(amountCents, currency); } } toString() gives a human-readable form, equals() compares both fields for value equality (with the reflexive this == o shortcut), and hashCode() derives from the same two fields via Objects.hash(), keeping the equals/hashCode contract intact.

What does this print, and why?

Both print false. Box does not override equals(), so it inherits Object's identity-based default, which behaves exactly like ==. Even though a and b have the same size value, they are two distinct objects, so both comparisons are false.

Write a small program demonstrating Object.getClass() being used to implement a strict equals() that requires the exact same runtime class (not just any subtype).

class Shape { @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; return true; // no fields to compare in this simplified example } } class Circle extends Shape { } public class Main { public static void main(String[] args) { Shape s = new Shape(); Circle c = new Circle(); System.out.println(s.equals(c)); // false: different runtime classes } } getClass() != o.getClass() rejects any comparison between objects of different exact runtime types, even when one is a subclass of the other — stricter than an instanceof-based check, which would accept subtypes.

Prints 99. super.clone() performs a shallow copy: c2 gets its own Counter object, but c2.data still references the very same int[] array as c1.data. Mutating c2.data[0] therefore also changes what c1.data[0] reports.

class Box { int size; }

public class Main {
    public static void main(String[] args) {
        Box a = new Box();
        Box b = new Box();
        a.size = 5;
        b.size = 5;
        System.out.println(a.equals(b));
        System.out.println(a == b);
    }
}
class Counter implements Cloneable {
    int[] data = {1, 2, 3};
    @Override
    public Counter clone() throws CloneNotSupportedException {
        return (Counter) super.clone();
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Counter c1 = new Counter();
        Counter c2 = c1.clone();
        c2.data[0] = 99;
        System.out.println(c1.data[0]);
    }
}