equals(), hashCode(), toString() — The Contract

equals, hashCode, toString — Java-র সবচেয়ে গুরুত্বপূর্ণ চুক্তি

Read: ~32 min Advanced 5 practice problems Live code runner

1. Why Every Java Dev Must Know This

Three methods on Object get called constantly by the standard library, most often without you realising it: equals() (compared by HashMap, HashSet, List.contains, .equals itself), hashCode() (used as the bucket index in every hash-based collection), and toString() (used whenever you log, print, or concatenate an object with a string). Get them wrong and your objects silently misbehave: keys vanish from maps, sets hold duplicates, logs are unreadable.

Object-এর তিনটি method — equals, hashCode, toString — Java standard library প্রতিনিয়ত ডাকে, প্রায়ই আপনি না জেনে। ভুল করলে: HashMap key খুঁজে পায় না, Set-এ duplicate থাকে, log পড়া যায় না। ঠিকঠাক বানানো অপরিহার্য।

2. The Contract in Five Rules

equals must satisfy:

  1. Reflexive — x.equals(x) is always true.
  2. Symmetric — if x.equals(y), then y.equals(x).
  3. Transitive — if x.equals(y) and y.equals(z), then x.equals(z).
  4. Consistent — repeated calls return the same result as long as nothing relevant changes.
  5. x.equals(null) is always false.

And the critical link to hashCode:

If a.equals(b) is true, then a.hashCode() == b.hashCode() must be true.
যদি a.equals(b) true হয়, a.hashCode() ও b.hashCode() একই হতে হবে। ভাঙলে HashMap/HashSet সঠিকভাবে কাজ করবে না।

The reverse is not required — two unequal objects may share a hash code (a "collision"), and hash-based collections cope with that. But equal objects producing different hashes is a silent bug.

3. A Correct Implementation — with Objects.equals & Objects.hash

The java.util.Objects helper class does the boring work. Always start with it — it handles nulls and gives you compact code.

Main.java
import java.util.Objects;
import java.util.HashMap;

final class NID {
    private final String number;
    private final String name;

    NID(String number, String name) {
        this.number = number;
        this.name   = name;
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;                   // quick identity check
        if (!(o instanceof NID other)) return false;   // type + null in one
        return Objects.equals(number, other.number)
            && Objects.equals(name,   other.name);
    }

    @Override public int hashCode() { return Objects.hash(number, name); }

    @Override public String toString() { return "NID[" + number + ", " + name + "]"; }
}

class Main {
    public static void main(String[] args) {
        NID a = new NID("1234567890", "Sumi");
        NID b = new NID("1234567890", "Sumi");
        System.out.println(a.equals(b));      // true
        System.out.println(a.hashCode() == b.hashCode()); // true — contract held

        HashMap<NID, String> db = new HashMap<>();
        db.put(a, "Dhaka");
        System.out.println(db.get(b));        // "Dhaka" — found via b
    }
}

4. What Happens If You Override Only equals?

Forgetting hashCode is the classic bug. Two logically-equal objects land in different buckets of the hash table, and the map loses them.

Main.java
import java.util.HashSet;

class Broken {
    int id;
    Broken(int id) { this.id = id; }
    @Override public boolean equals(Object o) {
        return o instanceof Broken b && b.id == id;
    }
    // NOTE: no hashCode() — inherits Object's identity-based default
}

class Main {
    public static void main(String[] args) {
        HashSet<Broken> s = new HashSet<>();
        s.add(new Broken(7));
        System.out.println(s.contains(new Broken(7)));
        // Almost always prints FALSE — hashes differ even though equals() says yes.
    }
}
শুধু equals override করলে কিন্তু hashCode নয় — HashSet-এ যোগ করা object contains-এ পাওয়া যাবে না। দুটো একসাথে override করা বাধ্যতামূলক।

5. Records Solve This for You

A record auto-generates equals, hashCode, and toString from its components — correctly, by value. That is one of the strongest reasons to reach for records for simple data-carrying types.

Main.java
record NID(String number, String name) {}

class Main {
    public static void main(String[] args) {
        NID a = new NID("1234567890", "Sumi");
        NID b = new NID("1234567890", "Sumi");
        System.out.println(a.equals(b));   // true
        System.out.println(a);             // NID[number=1234567890, name=Sumi]
    }
}

6. Vocabulary (শব্দভাণ্ডার)

TermMeaningবাংলায়
Reference equalityDefault — same object in memory (==).একই object — default।
Value equalityWhat a correct equals expresses — "same content".একই বিষয়বস্তু — সঠিক equals।
hashCode()Integer digest of the object; must match for equals objects.Object-এর integer digest; equal হলে hashCode-ও সমান হতে হবে।
Hash collisionDifferent objects producing the same hash — allowed.ভিন্ন object-এর একই hash — অনুমোদিত।
Objects.equalsNull-safe equality helper.Null-safe equality helper।
Objects.hashBuilds a good hash from several components.কয়েকটি component থেকে ভালো hash।
toString()Human-readable form — used in logs and concatenation.মানুষের পড়ার জন্য form।

7. Practice Problems

  1. Write a class Coord with fields x, y, correct equals, hashCode, and toString. Show that a HashSet detects duplicates.
    Coord class বানান — x, y field, সঠিক equals, hashCode, toString। HashSet duplicate ধরে কিনা দেখান।
    ✨ Show Answer
    Main.java
    import java.util.HashSet;
    import java.util.Objects;
    final class Coord {
        final int x, y;
        Coord(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            return o instanceof Coord c && c.x == x && c.y == y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
        @Override public String toString() { return "(" + x + "," + y + ")"; }
    }
    class Main {
        public static void main(String[] args) {
            HashSet<Coord> s = new HashSet<>();
            s.add(new Coord(1,2));
            s.add(new Coord(1,2));
            System.out.println(s.size()); // 1
        }
    }
  2. Two objects have equals true but different hashCode. Which part of the contract is broken and what practical consequence follows?
    দুটি object equals true, কিন্তু hashCode আলাদা — contract-এর কোন অংশ ভেঙেছে, বাস্তব সমস্যা কী?
    ✨ Show Answer

    Answer: The "equal ⇒ same hash" rule of the hashCode contract is broken. Practically, any hash-based collection (HashMap, HashSet, LinkedHashMap) will look for the object in the wrong bucket, so contains/get will report "not present" even though equals says it is. This is a silent correctness bug.

    "Equal হলে hash-ও equal" নিয়মটি ভাঙছে। ফলে HashMap/HashSet ভুল bucket-এ খুঁজবে — contains/get false দেবে, অথচ equals true। নীরব (silent) bug।

  3. Convert this class to a record and show that the behaviour is identical: class P { int a; int b; ... }.
    এই class-টিকে record-এ রূপান্তর করে দেখান আচরণ একই: class P { int a; int b; ... }।
    ✨ Show Answer
    Main.java
    record P(int a, int b) {}
    class Main {
        public static void main(String[] args) {
            System.out.println(new P(1,2).equals(new P(1,2)));
            System.out.println(new P(1,2));
        }
    }
  4. Why should equals start with if (this == o) return true;?
    equals-এ প্রথমে if (this == o) return true; কেন?
    ✨ Show Answer

    Answer: It is a cheap optimisation: reference identity is the strongest possible form of equality, so if it holds we can skip all field comparisons. Since equals(this) is called surprisingly often (especially inside collection operations), this one-line short-circuit matters.

    Reference identity সবচেয়ে শক্তিশালী equality — true হলে field তুলনা করার দরকার নেই। Collection operations-এ বারবার কল হয় বলে এই short-circuit কার্যকর।

  5. State one rule you must never break when implementing hashCode for a mutable class that is used as a HashMap key.
    Mutable class যদি HashMap key হয়, hashCode বানানোর সময় কোন একটি নিয়ম কখনো ভাঙা যাবে না — বলুন।
    ✨ Show Answer

    Answer: Never include a field in hashCode whose value can change while the object is a key in a hash-based collection. If the field changes, the object's hash changes, and the collection will look for it in the wrong bucket — effectively losing it. Either make the equals-relevant fields final, or do not mutate them while the object is in a map/set.

    Object যখন HashMap key, তখন hashCode-এ ব্যবহৃত কোনো field পরিবর্তন করা যাবে না — করলে collection সেটিকে ভুল bucket-এ খুঁজবে, অর্থাৎ "হারিয়ে" যাবে।

Summary — Module 19

Every time you put an object into a HashMap or compare two instances with .equals, you rely on the equals/hashCode contract. Override them together, never just one. Use Objects.equals and Objects.hash as the safe default. toString is not part of the contract but hugely improves debugging and logs. For simple data carriers, a record gives all three correctly for free — and that alone is a great reason to use them.

equals/hashCode contract Java-র সবচেয়ে গুরুত্বপূর্ণ চুক্তি। দুটিই একসাথে override করুন, কখনো একটিকে নয়। Objects.equals ও Objects.hash নিরাপদ default। toString চুক্তির অংশ না হলেও log ও debug-এ অমূল্য। সাধারণ data carrier-এর জন্য record তিনটিই সঠিকভাবে বিনামূল্যে দেয়।

Next Module → Object class, Cloneable ও Comparable।