Menu

Object Class and Key Methods in Java

Object Class and Key Methods

The Intermediate tutorial introduced overriding toString(), equals(), and hashCode() individually. This chapter treats them as one interconnected unit, the contract every well-behaved Java class is expected to honour, and then goes deeper into cloning, the one Object method most developers get wrong on a first attempt.

toString(), equals(), and hashCode()

These three methods exist on every Java object because every class ultimately inherits from java.lang.Object. Their default behaviour, untouched, treats every object as fundamentally unique: printed as a class name plus a hash, equal only to itself by reference, and hashed based on its memory identity.

Method

Default (Unoverridden) Behaviour

Why You Usually Override It

toString()

Returns ClassName@hexHashCode, e.g. Order@4554617c

To produce readable, meaningful output when logging or debugging

equals(Object o)

True only if both references point to the same object in memory

To compare objects by their meaningful field values instead of identity

hashCode()

An integer derived from the object's identity, unrelated to its field values

To keep it consistent with a custom equals(), required for correct use in hash-based collections

These three are not independent choices, they form a single contract. The moment you override equals() to compare by value, you are obligated to override hashCode() the same way, or you silently break every HashMap, HashSet, and HashTable that ever stores your objects.

Overriding Object Class Methods

A correct, contract-respecting override of all three methods together generally looks like this:

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 String.format("%d.%02d %s", amountCents / 100, amountCents % 100, currency);

}

@Override

public boolean equals(Object o) {

if (this == o) return true;

if (o == null || getClass() != o.getClass()) return false;

Money other = (Money) o;

return amountCents == other.amountCents && currency.equals(other.currency);

}

@Override

public int hashCode() {

return Objects.hash(amountCents, currency);

}

}

Notice the getClass() != o.getClass() check rather than an instanceof check. This is a deliberate choice: instanceof would allow a subclass instance to equal a superclass instance, which can quietly violate the equals() contract's symmetry requirement, a.equals(b) must always agree with b.equals(a), once inheritance and added fields enter the picture. The Effective Java guidance from Joshua Bloch is to favour composition over extending a concrete, equals()-overriding class specifically to avoid this trap.

•        Reflexive: x.equals(x) must always be true.

•        Symmetric: x.equals(y) must return the same result as y.equals(x).

•        Transitive: if x.equals(y) and y.equals(z), then x.equals(z) must also be true.

•       Consistent: repeated calls to x.equals(y) must keep returning the same result, as long as neither object's relevant fields change.

•        Never equal to null: x.equals(null) must always return false.

2.3 clone() and Shallow vs Deep Copy

clone() is the most failure-prone of the Object methods, largely because its default implementation performs a shallow copy: primitive fields are duplicated directly, but any field that is itself a reference to another object is copied as a reference, not as an independent new object. The clone and the original end up secretly sharing that nested object.

class Address {

String city;

Address(String c) { city = c; }

}

class Person implements Cloneable {

String name;

Address address;  // a reference field — this is where shallow copy bites

Person(String n, Address a) {

name = n;

address = a;

}

@Override

public Person clone() throws CloneNotSupportedException {

return (Person) super.clone();   // SHALLOW — address is shared, not duplicated

}

}

Person original = new Person("Asha", new Address("Chennai"));

Person copy = original.clone();

copy.address.city = "Mumbai";

System.out.println(original.address.city);   // Mumbai — the 'original' was affected too!

That last line is the classic shallow-copy trap: modifying the clone's address field unexpectedly altered the original, because both objects were pointing at the exact same Address instance the entire time. A deep copy fixes this by explicitly cloning every nested mutable field as well, not just the top-level object.

class Address implements Cloneable {

String city;

Address(String c) { city = c; }

@Override

public Address clone() {

return new Address(city);   // Strings are immutable, so a new wrapper is enough

}

}

class Person implements Cloneable {

String name;

Address address;

Person(String n, Address a) {

name = n;

address = a;

}

@Override

public Person clone() {

Person copy = new Person(name, address.clone());   // DEEP — address is duplicated too

return copy;

}

}

Aspect

Shallow Copy

Deep Copy

Primitive fields

Duplicated independently

Duplicated independently

Reference fields (objects)

Shared between original and clone

Recursively cloned, fully independent

Risk

Mutating the clone can unexpectedly affect the original

No shared mutable state between original and clone

Performance

Cheaper, fewer objects created

More expensive, every nested mutable object is duplicated

Quick Tip

Many experienced Java developers avoid the built-in Cloneable mechanism almost entirely, since it is widely considered one of the language's more awkward corners (clone() throws a checked exception inconsistently, and Cloneable carries no actual clone() method itself to enforce). A copy constructor, a constructor that takes another instance of the same class and copies its fields, is often a cleaner, more explicit alternative for achieving the same result.