Every Java class extends java.lang.Object, so every object has its methods: equals, hashCode, toString, getClass, clone, wait, notify and more. Overriding equals and hashCode correctly matters in test code too: it decides whether two POJOs from an API response compare as equal and whether they work as HashMap keys.

// Every Java class implicitly extends Object
// Object has 11 methods:

Object obj = new Object();

// 1. equals(Object o) — logical equality (default: reference ==)
obj.equals(other); // override for value comparison

// 2. hashCode() — for hash-based collections
obj.hashCode(); // default: memory-based (JVM implementation-specific)

// 3. toString() — string representation
obj.toString(); // default: ClassName@hexHashCode

// 4. getClass() — runtime class
Class<?> cls = obj.getClass(); // exact runtime type
cls.getName();        // fully qualified name
cls.getSimpleName();  // just class name

// 5. clone() — shallow copy (protected, must override)
// Requires implementing Cloneable marker interface
// Better to use copy constructor instead

// 6. finalize() — called before GC (deprecated since Java 9, marked for removal in Java 18)
// Never use — unpredictable, may delay GC
// Use Cleaner or try-with-resources instead

// 7. wait() — release lock and wait
synchronized(obj) { obj.wait(); }       // wait indefinitely
synchronized(obj) { obj.wait(1000); }   // wait max 1s
synchronized(obj) { obj.wait(1000, 500000); } // 1s + nanoseconds

// 8. notify() — wake one waiting thread
synchronized(obj) { obj.notify(); }

// 9. notifyAll() — wake all waiting threads
synchronized(obj) { obj.notifyAll(); }
// Must hold object's monitor (synchronized) to call wait/notify/notifyAll!

// 10. equals contract rules:
// Reflexive: x.equals(x) must be true
// Symmetric: x.equals(y) ↔ y.equals(x)
// Transitive: x.equals(y) && y.equals(z) → x.equals(z)
// Consistent: repeated calls return same result
// Null: x.equals(null) must be false (not NPE)

// Correct equals() implementation:
@Override
public boolean equals(Object o) {
    if (this == o) return true;              // same reference
    if (!(o instanceof Person p)) return false; // null-safe + type check
    return age == p.age && Objects.equals(name, p.name); // field comparison
}

@Override
public int hashCode() {
    return Objects.hash(name, age); // consistent with equals
}

Why This Matters in Tests

// Without equals(): two users with identical data are NOT equal
User expected = new User(7, "Asha", "asha@example.com");
User actual = api.getUser(7);
assertEquals(expected, actual);        // fails unless User overrides equals()

// Records generate equals, hashCode and toString for you
record User(int id, String name, String email) { }
assertEquals(expected, actual);        // passes when all fields match

// toString() decides what your failure message shows
// without it: expected: User@1b6d3586 but was: User@4554617c
// with it:    expected: User[id=7, name=Asha, ...] but was: User[id=7, name=Asha K, ...]

For POJOs used in assertions, either use records, generate equals/hashCode/toString with your IDE or Lombok, or compare field by field with AssertJ's usingRecursiveComparison().

Advertisement

FAQs

Why must you override hashCode when you override equals?

Equal objects must have equal hash codes. If only equals is overridden, two equal objects can land in different HashMap buckets and lookups fail.

What does the default toString() return?

The class name, an @ sign and the hash code in hex, for example User@1b6d3586; override it to show useful field values.

Why is clone() discouraged?

It makes shallow copies, requires the Cloneable marker interface and a cast, and is easy to get wrong; copy constructors, static factory methods or records are clearer.