Object Superclass

Every Class's Ultimate Parent

Object (from java.lang) is the superclass of every class in Java — any class without an explicit extends inherits from it automatically. Three of Object's methods matter enough to override in almost every class you write: toString(), equals(Object other), and — always together with equals — hashCode().

This lesson replaces inherited methods with new versions, which is called overriding, and marks each one with @Override. Lesson 18.1 covers overriding in full; for now, @Override just tells the compiler "this method replaces one I inherited," and the compiler double-checks that it really does.

Overriding toString()

Object's default toString() produces an unhelpful string like RobotPart@1a2b3c (the class name, @, and the object's hash code in hex). Overriding it to describe the object's real state is good practice in nearly every class. Here's what RobotPart (Lesson 17.1) adds:

public class RobotPart
{
    // ...name field, constructor, getName(), log(), and stop() as in Lesson 17.1

    @Override
    public String toString() { return name; }
}

A subclass's own toString() can call super.toString() — "the superclass's version of this method" (Lesson 18.2 covers super.method() in full) — and build on it, rather than starting from scratch:

public class Intake extends RobotPart
{
    private boolean hasGamePiece;

    public Intake(String name) { super(name); }

    @Override
    public String toString()
    {
        return super.toString() + " (hasGamePiece=" + hasGamePiece + ")";
    }
}

The equals() Method

equals(Object other), inherited from Object, tests whether two references point to the exact same object — the same result as == (the "== vs. .equals()" gotcha from Lesson 6.1 and Ch.13 exists precisely because many classes, like String, override this default with something more useful).

Checking and Casting Object Types

Overriding equals needs two tools for working with an Object-typed parameter. x instanceof RobotPart is true when the object x refers to is a RobotPart or any subclass of one (and false when x is null). A cast like (RobotPart) other tells the compiler to treat an Object reference as a RobotPart reference, so RobotPart's fields and methods can be used through it — it doesn't change the object at all. If the object isn't really a RobotPart, the cast throws a ClassCastException, which is why the instanceof check comes first. Lesson 18.3 covers casting between class types in full.

Overriding equals() Correctly

Overriding equals is more involved than toString, since it has to satisfy five formal requirements to remain safe to use anywhere: reflexive (o.equals(o) is always true), symmetric (a.equals(b) matches b.equals(a)), transitive (a.equals(b) and b.equals(c) implies a.equals(c)), consistent (repeated calls give the same answer, if nothing changed), and never equal to null.

A standard recipe, added to RobotPart, satisfies all five:

@Override
public boolean equals(Object other)
{
    if (this == other) return true;                   // 1. quick check: same object
    if (!(other instanceof RobotPart)) return false;  // 2. must be the same type
    RobotPart o = (RobotPart) other;                  // 3. safe to cast now
    return this.name.equals(o.name);                  // 4. compare the relevant fields
}

Since Java 16, steps 2 and 3 can be combined into one line with a pattern: if (!(other instanceof RobotPart o)) return false; checks the type and, when the check passes, gives you o already cast to RobotPart. Both versions do exactly the same thing.

Note the parameter type is always Object, never the class itself. equals(RobotPart other) would be a completely different method (overloading, not overriding): it runs only when the argument's declared type is RobotPart. Library code — ArrayList.contains, HashMap, HashSet — always calls equals(Object), which would still be Object's identity check. The bug stays silent until a collection gives a wrong answer, and @Override is what catches it: on equals(RobotPart other) it's a compile error.

equals() and hashCode() Go Together

Object has a third method tied to equals: hashCode(), which returns an int that HashMap and HashSet (Ch.9, Ch.15) use to decide where to store and look for an object. The rule: two objects that are equals must return the same hashCode(). Object's default hashCode() gives every separate object its own value, so a class that overrides only equals breaks hash-based collections:

HashSet<RobotPart> parts = new HashSet<RobotPart>();
parts.add(new RobotPart("Front Intake"));
// false without a hashCode() override, even though equals() says they match
System.out.println(parts.contains(new RobotPart("Front Intake")));

The fix is to override hashCode() whenever you override equals, built from the same fields equals compares:

@Override
public int hashCode()
{
    return name.hashCode();  // same name -> same hash code
}

With that added to RobotPart, the contains call above prints true. For a class whose equals compares several fields, return Objects.hash(field1, field2); (from java.util.Objects) combines them.

The Subclass equals() Trap

Overriding equals again in a subclass of a class that's already overridden it is genuinely difficult to get right, and easy to accidentally break symmetry. If Intake (subclass of RobotPart) overrode equals to also compare hasGamePiece, then genericPart.equals(intakeObj) would call RobotPart's equals (comparing only name), while intakeObj.equals(genericPart) would call Intake's equals — which checks other instanceof Intake and returns false, since a plain RobotPart isn't an Intake. The two calls disagree, breaking the symmetry rule. The general guideline: only one class in a hierarchy should provide a real equals override.

Common Pitfalls

  • Writing equals(RobotPart other) instead of equals(Object other). That's overloading a new method, not overriding Object's — it only runs for RobotPart-typed arguments, while collections like ArrayList and HashSet still call Object's original identity check. @Override turns this mistake into a compile error.
  • Overriding equals without hashCode. Two equal objects then usually have different hash codes, so HashSet.contains and HashMap.get quietly fail to find them. Override both, using the same fields.
  • Forgetting the instanceof check before casting. Casting other to your class without checking first throws ClassCastException if other turns out to be some unrelated type.
  • Overriding equals again further down an inheritance hierarchy. This tends to violate symmetry — safest to let only one class in the hierarchy own the equals override.

Key Takeaways

  • Object is the superclass of every class in Java; toString(), equals(Object other), and hashCode() are its methods worth overriding almost everywhere.
  • A subclass's toString() can call super.toString() and extend it, rather than rebuilding the whole description from scratch.
  • Object's default equals is identity comparison (same as ==); a correct override must be reflexive, symmetric, transitive, consistent, and never true against null.
  • The standard equals recipe: check this == other, check instanceof, cast (or use the instanceof RobotPart o pattern), then compare the relevant fields — always with parameter type Object.
  • Whenever you override equals, override hashCode from the same fields — equal objects must have equal hash codes.
  • Avoid overriding equals more than once down an inheritance hierarchy — it tends to break symmetry.

Derived from csawesome-2026: unit-5-inheritance-optional/5.07-object-superclass.md (CSA 5.7)