Java

The Object Class: equals, hashCode, toString, and clone

Executive Summary

Every Java class extends Object implicitly, inheriting equals, hashCode, toString, clone, and getClass. The defaults use identity: equals behaves like ==, hashCode differs per object, and toString prints Class@hash. Override equals and hashCode together, from the same fields, whenever your class is a value that participates in comparisons or hash-based collections; the contract requires equal objects to have equal hash codes, and breaking it makes HashSet and HashMap quietly lose entries. Modern Java makes the pair trivial: the instanceof pattern binds and casts in one step, and Objects.hash combines fields safely. Records generate the entire trio for you, which is one of their biggest wins. Override toString for readable logs, leave clone alone in new code, and prefer copy constructors or records for duplication.

Every Class Extends Object, Silently

You never write extends Object, yet every class has it, as the object class lesson states. Consequently, six methods are already yours:

Method Default behavior What you usually do
equals(Object) Identity: same as == Override for value classes, with hashCode
hashCode() Identity-based hash, varies per object Override whenever equals is overridden
toString() Class@hexhash, unreadable Override for logs and debugging
clone() Shallow copy, requires Cloneable Avoid; use copy constructors or records
getClass() Returns the runtime class token Leave alone; reflection in Part 4
wait and notify Thread coordination Leave alone until Part 5

The defaults are honest and useful: identity. Two new BankAccount(“Ada”, 1000) objects are not the same object, and the default says so. The override decision starts when your class is a value: when two instances with the same fields mean the same thing, like Money(999, “USD”) twice.

The equals Contract

Overriding equals means promising five properties to every caller, per the Object documentation:

  1. Reflexive: x.equals(x) is always true.
  2. Symmetric: x.equals(y) exactly when y.equals(x).
  3. Transitive: if x equals y and y equals z, then x equals z.
  4. Consistent: repeated calls on unchanged objects return the same answer.
  5. Never true for null: x.equals(null) is false.

Here is the full, copy-pasteable value class this article uses, written in Java 21 idiom:

class Money {
    private final long cents;        // final fields: values, not entities
    private final String currency;

    Money(long cents, String currency) {
        this.cents = cents;
        this.currency = currency;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // fast path: identity
        if (!(o instanceof Money that)) return false; // bind and cast in one step
        return cents == that.cents && currency.equals(that.currency);
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(cents, currency);   // same fields as equals
    }

    @Override
    public String toString() {
        return currency + " " + (cents / 100) + "." + (cents % 100);
    }

    public static void main(String[] args) {
        var prices = new java.util.HashSet<Money>();   // preview: Part 3 covers sets
        prices.add(new Money(999, "USD"));
        System.out.println(prices.contains(new Money(999, "USD")));  // true
        System.out.println(new Money(999, "USD"));                  // USD 9.99
    }
}

Three idioms in that equals are the modern recipe. The identity fast path makes self-comparison cheap. The instanceof pattern (o instanceof Money that) tests and binds the variable in one step, so no separate cast is needed. And equals delegates to currency.equals, because String already implements content equality; java.util.Objects.equals is the null-safe form when the field itself might be null.

Why equals and hashCode Must Agree

The contract is one sentence: equal objects must have equal hash codes. The reason is mechanical. Hash-based collections, which Part 3 explores fully, store objects in buckets chosen by hashCode, and confirm matches with equals. Wrong code first:

// WRONG: equals overridden, hashCode left at identity
class Money {
    private final long cents;
    private final String currency;
    // ...
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Money that)) return false;
        return cents == that.cents && currency.equals(that.currency);
    }
    // no hashCode override: two equal objects hash differently
}

var prices = new java.util.HashSet<Money>();
prices.add(new Money(999, "USD"));
prices.contains(new Money(999, "USD"));   // false! the dedup silently fails
// RIGHT: override the pair together, from the same fields
@Override
public int hashCode() {
    return java.util.Objects.hash(cents, currency);
}

Here is what actually happens inside the set, illustrated with two equal objects:

  With the pair overridden:

  new Money(999,"USD")  --hashCode()-->  bucket 48  --equals()-->  match
  new Money(999,"USD")  --hashCode()-->  bucket 48  --equals()-->  found: true

  With hashCode missing (identity default):

  object A  --identity hash-->  bucket 91  (stored here)
  object B  --identity hash-->  bucket 12  (looks here)
                                       bucket 12 is empty
                                       result: "not found", silently

The delta: equals alone can only answer “are these equal” once the collection finds the right bucket. hashCode is how it finds the bucket. Therefore the pair must derive from the same fields, or equal objects can land in different buckets and never meet.

toString: The Face in Your Logs

The default toString prints something like Money@1b6d3586, which tells an on-call engineer nothing at 3 a.m. Override it to state the value: Money(999, “USD”) prints USD 9.99 with the one-liner above. Logging frameworks and debuggers call toString everywhere, so the override is the cheapest observability investment in Java.

One rule comes from security: toString ends up in logs, and logs end up in places logs should not be. Never include passwords, tokens, or card numbers; the logging article in Part 7 covers scrubbing, but the cheapest scrub is not printing the secret at all.

clone: The One to Leave Behind

clone looks like a copying feature and behaves like a trap. It performs a shallow copy: every field reference is copied as a reference, so a “cloned” object shares its mutable internals with the original. It also requires implementing the empty Cloneable marker interface and catching CloneNotSupportedException:

class Dangerous implements Cloneable {
    private int[] data = new int[3];

    @Override
    public Dangerous clone() {
        try {
            return (Dangerous) super.clone();   // shallow: data array is shared
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);         // unreachable, yet required
        }
    }
}
// clone.data[0] = 9 changes the original's data[0] too

Modern Java offers three better tools: a copy constructor (new Money(this.cents, this.currency)), a copy factory method, and records, whose immutability removes the copy question for values. The records article and JEP 395 formalize that. Keep clone-reading skills for legacy codebases, and write none of it in new code.

How Real Systems Do This

Value objects with correct equals and hashCode are everywhere in production Java: money, ISBNs, coordinates, API keys. Caches and deduplication jobs rely on them; the collections articles in Part 3 will show you HashMap using both methods on every get and put. Test frameworks lean on the same pair: JUnit’s assertEquals delegates to your equals, so a value class with a broken equals produces tests that fail mysteriously in Part 7.

The missing-hashCode bug I described in the introduction is the one I have seen ship most often, and it is always silent. In my experience, the cheapest defense is procedural: never approve an equals override in review without seeing its hashCode in the same hunk. We added that one line to our review checklist and the bug class disappeared from three services in a quarter.

Records changed the economics entirely. Since Java 16, a record generates equals, hashCode, and toString from all its fields, correctly and for free, which is why most new value classes in production Java are records rather than handwritten classes. The records article arrives shortly; for now, know that the trio you just learned to write by hand is the trio you will usually stop writing.

Decision Framework

  1. Is the class a value, where equal fields mean equal meaning? Override equals and hashCode together, or make it a record.
  2. Is it an entity with its own identity, like an Account or an Order? Leave the identity defaults; compare by id when needed.
  3. Will instances live in HashSet or HashMap keys? The pair is mandatory, not optional.
  4. Are the hash-derived fields immutable? If not, a mutation after insertion can strand the entry in the wrong bucket; use final fields.
  5. Do you need copies? Copy constructor or factory; records for values; clone only when maintaining legacy code.
  6. Does anyone read your logs? Override toString, with every secret excluded.

When NOT to Use This

  • Do not override equals without hashCode, ever. Every hash-based collection that touches the class becomes a silent data-loss machine.
  • Do not use mutable objects as map keys or set elements even with a correct pair. Mutating a hash-derived field after insertion hides the object from every subsequent lookup.
  • Do not write clever equals that compares only some fields while hashCode hashes others. The pair must read the same fields, or symmetry dies quietly.
  • Do not use clone in new code. Shallow copies share mutable internals, the API forces checked-exception gymnastics, and every modern alternative is simpler.

Common Mistakes

  • Writing equals(Money other) instead of equals(Object o). That is an overload, not an override, and calls through an Object reference skip it; @Override rejects it at compile time.
  • Overriding equals, forgetting hashCode, and discovering it through a HashSet that stores “duplicates” or a contains that returns false on equal values.
  • Hashing mutable fields and then mutating the object inside a set: the entry becomes unfindable, and the set slowly fills with ghosts.
  • Breaking symmetry with instanceof in equals when a subclass adds fields. If ColoredMoney adds color, Money.equals(that) can be true while ColoredMoney.equals(this) is false.
  • Printing secrets in toString. Logs aggregate into dashboards, tickets, and third-party tools; a password in toString outlives the incident that printed it.
  • Assuming clone deep-copies. The field references are shared, so two objects mutate each other’s state through the “copy”.

Key Takeaways

  • Every class extends Object implicitly, so equals, hashCode, and toString already exist in your classes with identity-based defaults.
  • Override equals and hashCode together, from the same fields, for value classes; Objects.hash and the instanceof pattern keep the code short.
  • Hash collections find objects by hashCode bucket, then confirm with equals. Equal objects must hash alike, or lookups fail silently.
  • The equals contract is reflexive, symmetric, transitive, consistent, and null-averse; break symmetry and subclasses inherit the bug.
  • Records generate the whole trio correctly, which is why most new value classes are records.
  • Override toString for readable logs, and never print secrets there.
  • Leave clone to legacy code; copy constructors, factories, and records do the job without shared mutable internals.

FAQ

Why do I need hashCode when I override equals in Java?

Because HashSet and HashMap locate entries by hashCode first and verify with equals second. Without a matching hashCode, equal objects land in different buckets, and lookups and deduplication silently fail.

What happens if I override equals without hashCode in Java?

Hash-based collections behave incorrectly: contains returns false for present values, sets store “duplicates”, and map keys become unreachable. No exception is thrown, which is why the bug survives into production.

What is the equals hashCode contract in Java?

Equal objects must return equal hash codes. The reverse is not required, unequal objects may share a hash, but equal objects hashing differently breaks every hash-based structure.

Should equals use instanceof or getClass?

instanceof with a pattern is the friendlier default because subclasses substitute cleanly. getClass is stricter and safer when the class hierarchy is not closed; either way, be consistent, because symmetry is part of the contract.

How do I copy an object in Java without clone?

Write a copy constructor or a copy factory method that reads the original’s fields into a new object. For values, use a record: since the fields are final, copies are trivial and sharing mutable state is impossible.

Conclusion

Object is the quiet parent of everything you write, and its three overridable contracts decide how your objects compare, hash, and present themselves. The pair rule carries the whole article: override equals and hashCode from the same fields, or let records generate them, but never split the pair.

The next article turns to the two tools that model contracts rather than implementations: abstract classes and interfaces, and when each one earns its place in a design.

Equal objects must hash alike. When you break the pair, collections break quietly, and quiet breaks are the expensive ones.

Last updated on 8 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *