সরাসরি পর্বে যান
পর্ব ২৩ · ৫৯-এর মধ্যে · মডিউল ৪

equals, hashCode, toString আর compareTo — Object-এর চুক্তি

Java equals, hashCode, toString and compareTo — The Contracts Every Object Inherits

Taught in Bangla, this lesson covers the Object contract: writing useful toString, == versus equals, the five rules of the equals contract and two classic traps, how HashSet uses hashCode and why overriding equals demands hashCode, mutable keys, Comparable and compareTo without subtraction overflow, and generating these with IDEs or records.

এই পর্বে যা শিখবেন

  • Object class-এর method-গুলো কী কী, আর কোনগুলো আপনাকে নিয়মিত override করতে হবে
  • উপযোগী toString() লেখা, আর == বনাম equals-এর আসল পার্থক্য
  • equals contract-এর পাঁচ শর্ত, সঠিক equals লেখার ধাপ, আর দুটি বিখ্যাত ফাঁদ (overload ভুল, inheritance-এর symmetry)
  • HashSet কীভাবে hashCode দিয়ে ঘর খোঁজে আর equals দিয়ে নিশ্চিত হয় — hashCode ভুললে কী ভাঙে
  • Comparable আর compareTo, overflow-এর bug, Arrays.sort, আর IDE/record/Lombok দিয়ে এগুলো তৈরি করা

১ · সবার উত্তরাধিকার — Object-এর কয়েকটি method

পর্ব ১৯-এ দেখেছেন, যে class-এ extends লেখা নেই, সেটাও আসলে java.lang.Object-এর subclass. তাই আপনার লেখা প্রতিটি class — Student, Book, Product — জন্ম থেকেই কয়েকটি method নিয়ে আসে। এতদিন এগুলো চুপচাপ পড়ে ছিল। কিন্তু Java-র বাকি অংশ, বিশেষ করে collection, এগুলোর ওপর অন্ধভাবে ভরসা করে। System.out.println(student) লিখলে println নিজে student-এর toString() ডাকে। list.contains(book) লিখলে ArrayList প্রতিটি উপাদানের সাথে equals() ডাকে। HashSet-এ কিছু রাখলে সে আগে hashCode() চায়।

অর্থাৎ এই method-গুলো আপনার class আর Java library-র মধ্যে একটি চুক্তি। Library ধরে নেয় আপনি নিয়ম মেনে লিখেছেন। নিয়ম ভাঙলে compiler কিছু বলে না, প্রোগ্রাম crash-ও করে না। শুধু HashSet-এ একই ছাত্র দুবার থাকে, লাইব্রেরির খোঁজে বই পাওয়া যায় না, sort-এর ক্রম মাঝে মাঝে উল্টে যায়। এই ধরনের নীরব bug খুঁজে পেতে দিন লেগে যায়। এই পর্বের লক্ষ্য তাই একটাই: চুক্তিগুলো বোঝা, যাতে নিজে লিখুন বা AI লিখুক, ভুলটা আপনি দেখতে পান।

জাতীয় পরিচয়পত্রের কথা ভাবুন। একজন মানুষকে চেনার তিনটি আলাদা প্রশ্ন আছে। “এঁর নাম কী, দেখতে কেমন?” — এটা toString, মানুষের পড়ার জন্য বর্ণনা। “এই দুটি কাগজ কি একই মানুষের?” — এটা equals, আর উত্তর মেলে NID নম্বর মিলিয়ে, ছবির রং বা কাগজের ভাঁজ দেখে নয়। “ভোটার তালিকার কোন খাতায় এঁর নাম খুঁজব?” — এটা hashCode: এলাকার কোড দেখে সঠিক খাতা খোলা, তারপর খাতার ভেতরে নাম মেলানো। আর চতুর্থ প্রশ্ন, “লাইনে কে আগে দাঁড়াবে?” — এটা compareTo. চারটি প্রশ্ন আলাদা, কিন্তু একটির উত্তর আরেকটির সাথে মিলতে হয়: একই মানুষের দুটি কাগজ যদি দুই এলাকার খাতায় খোঁজা হয়, তাকে কখনো পাওয়া যাবে না।

Object-এর public আর protected method-গুলোর পুরো তালিকা ছোট। কোনটি নিয়ে এই পর্বে কতটা কাজ, নিচের টেবিলে দেখুন:

Methodডিফল্টে কী করেআপনি কী করবেন
toString()class-এর নাম + @ + একটি hex সংখ্যাপ্রায় সব class-এ override করুন (অংশ ২)
equals(Object)== — একই object কি নামান দিয়ে তুলনা দরকার হলে override (অংশ ৩–৪)
hashCode()object-এর পরিচয় থেকে একটি intequals override করলে অবশ্যই (অংশ ৫–৬)
getClass()চলার সময় object-এর আসল classfinal, override করা যায় না
clone()protected; Cloneable থাকলে কপি বানায়সাধারণত এড়ান, copy constructor নিন (অংশ ৮)
finalize()GC-র আগে হয়তো ডাকা হতোব্যবহার করবেন না — deprecated
wait(), notify(), notifyAll()thread-দের মধ্যে সংকেতপর্ব ৪০-এ thread-এর সাথে

compareTo এই তালিকায় নেই — সেটা Object-এর নয়, আসে Comparable interface থেকে (অংশ ৭)। তবু এটাকে একই পরিবারে রাখা হয়, কারণ এরও একটি চুক্তি আছে আর সেটা equals-এর সাথে মিলতে হয়। Joshua Bloch তাঁর Effective Java বইয়ের একটি পুরো অধ্যায় (“Methods Common to All Objects”) এই চারটি আর clone নিয়ে লিখেছেন — Java-র পেশাদার জগতে এই অধ্যায়ের নিয়মগুলো প্রায় আইনের মতো মানা হয়।

এই পর্বে চারটি বাস্তব উদাহরণ বারবার আসবে:

Student (roll, name)

পরীক্ষার রেজিস্ট্রেশনের একটি HashSet. একই রোলের ছাত্র দুবার ফর্ম জমা দিলে set-এ একজনই থাকার কথা।

equals + hashCode

Book (isbn, title)

লাইব্রেরির খোঁজ: “এই বইটি কি তাকে আছে?” — shelf.contains(book) সঠিক উত্তর দেবে কি না।

equals-এর ফাঁদ

Money (amount, currency)

৫০০ টাকা মানে ৫০০ টাকা — কোন নোট, তা গুরুত্বহীন। মানই পরিচয়: একটি আদর্শ value object.

সঠিক equals

Product (name, price)

অনলাইন দোকানে পণ্য দামের ক্রমে সাজানো — সস্তা থেকে দামি।

compareTo

২ · toString — object-এর পড়ার মতো পরিচয়

সবচেয়ে সহজটি দিয়ে শুরু। toString() override না করে একটি Student ছাপলে যা পান, তার আকার এমন:

Student@(একটি hex সংখ্যা — প্রতিবার আলাদা হতে পারে)

Object-এর সংস্করণটি আসলে getClass().getName() + "@" + Integer.toHexString(hashCode()) ফেরত দেয়। Class-এর নামটুকু ছাড়া এতে কাজের কিছু নেই। সংখ্যাটি object-এর ঠিকানা নয় (অনেকে তাই ভাবেন) — সেটা ডিফল্ট hashCode, যা প্রতিবার প্রোগ্রাম চালালে বদলাতে পারে। একটি সমস্যার log-এ Payment failed for Student@5e91993f দেখে কিছুই বোঝা যায় না।

class Student {
    private final int roll;
    private final String name;
    Student(int roll, String name) { this.roll = roll; this.name = name; }

    @Override
    public String toString() {
        return "Student[roll=" + roll + ", name=" + name + "]";
    }
}
// System.out.println(new Student(7, "Rina"));  →  Student[roll=7, name=Rina]

এখন println, string জোড়া ("Got: " + s), debugger, log — সব জায়গায় পড়ার মতো লেখা আসে। কয়েকটি ব্যবহারিক নিয়ম:

  • সব গুরুত্বপূর্ণ field দেখান, যাতে log পড়ে object চেনা যায়। ClassName[field=value, …] ধরনটি প্রচলিত — record-ও ঠিক এই ধাঁচে নিজের toString বানায় (পর্ব ২৫)।
  • গোপন তথ্য কখনো নয়। পাসওয়ার্ড, PIN, পুরো কার্ড নম্বর, bKash-এর OTP toString-এ রাখলে সেটা log ফাইলে চলে যাবে, আর log ফাইল প্রায়ই অনেকে পড়ে।
  • toString-এর লেখাকে parse করবেন না। এটা মানুষের জন্য। অন্য প্রোগ্রামের দরকার হলে আলাদা getter বা JSON (পর্ব ৩৯) দিন, কারণ toString-এর ধরন যেকোনো দিন বদলাতে পারে।
  • বিশাল collection-ভরা field হলে সংক্ষেপ করুন — যেমন orders=1520 items, পুরো তালিকা নয়।
একটি ব্যতিক্রম: array

Array-ও object, কিন্তু array নিজের toString override করে না। তাই int[] ছাপলে আসে [I@ আর একটি hex সংখ্যা। Array-এর ভেতরের মান দেখতে Arrays.toString(arr) লিখুন (পর্ব ১৩)। একই কারণে দুটি array-এর তুলনায় a.equals(b) শুধু == করে — মান মেলাতে Arrays.equals(a, b).

৩ · equals — একই object, না একই মান?

পর্ব ০৭ আর ০৮-এ দেখেছেন, object-এর বেলায় == জিজ্ঞেস করে দুটি reference কি একই object দেখাচ্ছে — একে বলে reference equality. আর String-এর মান মেলাতে equals লাগে। কিন্তু String এটা পারে কারণ String class নিজে equals() override করেছে। আপনার Student যদি না করে, তাহলে Object-এর equals চলে, আর সেটা দেখতে এরকম:

public boolean equals(Object obj) {
    return (this == obj);      // java.lang.Object-এর আসল কোড, শুধু এটুকুই
}

অর্থাৎ new Student(7, "Rina").equals(new Student(7, "Rina")) হয় false — দুটি আলাদা object, রোল আর নাম এক হলেও। কখন এটাই ঠিক, আর কখন override দরকার?

ধরনউদাহরণequals
পরিচয়টাই আসল — প্রতিটি object অনন্যএকটি চলমান thread, একটি খোলা ফাইল, একটি bank account object যার ব্যালান্স বদলায়Object-এরটাই রাখুন (==)
মানটাই পরিচয় — value objectMoney (৫০০ BDT), তারিখ, স্থানাঙ্ক, ISBN-ওয়ালা Book, রোল-ওয়ালা Studentoverride করুন

equals contract — পাঁচটি শর্ত

equals override করা মানে Object-এর Javadoc-এ লেখা একটি চুক্তিতে সই করা। একে বলে equals contract. null নয় এমন যেকোনো x, y, z-এর জন্য:

শর্তমানেভাঙলে কী হয়
Reflexivex.equals(x) সবসময় truelist-এ রাখা object নিজেকেই খুঁজে পায় না
Symmetricx.equals(y) true হলে y.equals(x)-ও trueকে কাকে জিজ্ঞেস করছে, তার ওপর ফল বদলায় (অংশ ৪)
Transitivex = y আর y = z হলে x = z“সমান”-এর দল ভেঙে যায়, ডুপ্লিকেট সরানো ভুল হয়
Consistentobject না বদলালে বারবার ডাকলে একই উত্তরএকই প্রশ্নে কখনো হ্যাঁ, কখনো না
Non-nullx.equals(null) সবসময় false — exception নয়NullPointerException

শুনতে গণিতের মতো লাগলেও এগুলো সাধারণ বুদ্ধি: “সমান” শব্দটির কাছে আমরা যা আশা করি। বিপদ হলো, ভুল equals-ও দেখতে সম্পূর্ণ স্বাভাবিক লাগে।

সঠিক equals লেখার ধাপ — Money দিয়ে

import java.util.Objects;

final class Money {
    private final long poisha;        // টাকা নয়, পয়সায় — double-এর গোলমাল এড়াতে
    private final String currency;    // "BDT", "USD"
    Money(long poisha, String currency) {
        this.poisha = poisha;
        this.currency = Objects.requireNonNull(currency);
    }

    @Override
    public boolean equals(Object o) {                  // parameter Object — Money নয়!
        if (this == o) return true;                    // ১. একই object হলে সাথে সাথে হ্যাঁ
        if (!(o instanceof Money other)) return false; // ২. null বা অন্য টাইপ হলে না
        return poisha == other.poisha                  // ৩. গুরুত্বপূর্ণ প্রতিটি field মেলাও
            && currency.equals(other.currency);
    }

    @Override
    public int hashCode() { return Objects.hash(poisha, currency); }   // অংশ ৫
}

প্রতিটি লাইনের একটি কারণ আছে:

  • Parameter-এর টাইপ Object. Override করতে হলে signature হুবহু Object-এর মতো হতে হয়। equals(Money m) লিখলে override নয়, একটি নতুন overload — অংশ ৪-এর সবচেয়ে বিখ্যাত ফাঁদ।
  • this == o শুধু দ্রুততার জন্য: একই object হলে field মেলানোর দরকার নেই।
  • instanceof Money other — পর্ব ২০-এর pattern matching দিয়ে এক লাইনে দুই কাজ: টাইপ যাচাই আর cast. আর null instanceof Money সবসময় false, তাই non-null শর্তটি বিনা খরচে পূরণ হয়।
  • শুধু “গুরুত্বপূর্ণ” field. যে field পরিচয়ের অংশ, শুধু সেগুলো। Primitive field মেলান == দিয়ে (double/float হলে Double.compare), reference field মেলান equals দিয়ে।
  • Class-টি final. এতে কেউ subclass বানিয়ে নতুন field যোগ করে symmetry ভাঙতে পারে না (অংশ ৪)।

কোনো field null হতে পারলে name.equals(other.name) NullPointerException দিতে পারে। তখন লিখুন Objects.equals(name, other.name) — java.util.Objects class-এর এই method দুটিই null হলে true, একটি null হলে false দেয়। নামের মিল দেখে বিভ্রান্ত হবেন না: Object হলো সবার parent class, আর Objects (শেষে s) হলো Java 7-এ আসা একটি সাহায্যকারী class, যার সব method static.

instanceof না getClass()?

অনেক পুরোনো কোড আর কিছু IDE-র তৈরি কোডে দ্বিতীয় ধাপটি এভাবে থাকে:

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

getClass() চলার সময় object-এর হুবহু class দেয়। পার্থক্য দেখা যায় subclass এলে: instanceof বলে “Money বা Money-র যেকোনো subclass চলবে”, আর getClass বলে “হুবহু একই class হতে হবে”। Class final হলে দুটির ফল এক। Class final না হলে কোনটি নেবেন, সেটা একটি সত্যিকারের নকশার সিদ্ধান্ত, আর দুটিরই দাম আছে — অংশ ৪ আর ভাবনার প্রশ্ন ২-এ দেখবেন।

৪ · equals-এর দুই ফাঁদ — overload আর inheritance

ফাঁদ ১: equals(Book other) — override নয়, overload

লাইব্রেরির সফটওয়্যারে একজন লিখলেন — দেখতে একদম যুক্তিসঙ্গত:

class Book {
    private final String isbn;
    Book(String isbn) { this.isbn = isbn; }

    public boolean equals(Book other) {          // Object নয়, Book!
        return other != null && isbn.equals(other.isbn);
    }
}

a.equals(b) সরাসরি ডাকলে true আসে, তাই হাতে-কলমে পরীক্ষায় সব ঠিক মনে হয়। কিন্তু List.of(a).contains(b) আসে false! কারণ List-এর ভেতরের কোড লেখা আছে Object টাইপ দিয়ে, আর সে ডাকে equals(Object) — যেটা Book override করেনি। Book-এ এখন দুটি equals: Object থেকে পাওয়া equals(Object) (যা == করে) আর নতুন equals(Book). কোনটা চলবে, তা ঠিক হয় compile-এর সময়, ঘোষিত টাইপ দেখে (পর্ব ১২-এর overloading). Collection সবসময় প্রথমটি পায়। OpenJDK 21-এ চালিয়ে দেখা ফল: a.equals(b) true, কিন্তু Object o = b; a.equals(o) false, আর list-এর contains-ও false.

সমাধান এক শব্দের — @Override. পর্ব ১৯-এ দেখেছেন এটি compiler-কে মিলিয়ে দেখতে বলে। ওপরের method-এর মাথায় @Override বসালে javac সাথে সাথে ধরে:

Main.java:5: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error
মূল ধারণা

equals, hashCode, toString, compareTo — এই চারটির প্রতিটির মাথায় সবসময় @Override. এক শব্দে একটি পুরো শ্রেণির নীরব bug compile error-এ বদলে যায়। equals(Object o) — parameter-এর টাইপ অবশ্যই Object.

ফাঁদ ২: subclass নতুন field যোগ করলে symmetry ভাঙে

এবার একটি বিখ্যাত উদাহরণ, যেটা Bloch-এর বইয়ে আছে। Point(x, y)-এর equals দুটি স্থানাঙ্ক মেলায়, instanceof দিয়ে। কেউ ColorPoint extends Point বানালেন, সাথে একটি color field, আর ColorPoint-এর equals তিনটি জিনিস মেলায় — x, y আর রং। দুটিই আলাদাভাবে দেখলে ঠিক মনে হয়। চুক্তির শর্তগুলো একটি একটি করে যাচাই করুন:

কোডদুটি equals, দুটিই instanceof দিয়ে
class Point {
  equals(o): o instanceof Point p
             && x == p.x && y == p.y
}
class ColorPoint extends Point {
  equals(o): o instanceof ColorPoint cp
             && super.equals(cp) && color.equals(cp.color)
}
p  = new Point(1, 2)
cp = new ColorPoint(1, 2, "RED")
শর্ত ১Reflexive
p.equals(p)    → true  ✔
cp.equals(cp)  → true  ✔
নিজের সাথে নিজে সমান — এখানে সমস্যা নেই
শর্ত ২Symmetric
p.equals(cp)   → true   cp কি Point? হ্যাঁ। x, y মেলে।
cp.equals(p)   → false  p কি ColorPoint? না।
                       ✘  symmetry ভাঙল
OpenJDK 21-এ চালিয়ে যাচাই করা
ফলcollection-এ কী ঘটে
List<Point> a = [cp];   a.contains(p)  → true    (p.equals(cp))
List<Point> b = [p];    b.contains(cp) → false   (cp.equals(p))
একই দুটি বিন্দু — কে list-এ আর কে খুঁজছে,
তার ওপর উত্তর বদলায়
“সারানোর” চেষ্টাPoint পেলে রং বাদ দিয়ে মেলাও
red  = ColorPoint(1, 2, "RED")
p    = Point(1, 2)
blue = ColorPoint(1, 2, "BLUE")

red.equals(p)     → true   (রং বাদ)
p.equals(blue)    → true
red.equals(blue)  → false   ✘  transitivity ভাঙল
বাকি শর্তConsistent আর Non-null
field সব final → বারবার ডাকলে একই উত্তর    ✔ consistent
p.equals(null)  → false (instanceof null = false)  ✔ non-null

সমাধান: ColorPoint extends নয় —
ColorPoint-এর ভেতরে একটি Point field (composition),
অথবা Point-কে final করুন
  1. দুটি যুক্তিসঙ্গত equals. Point দুটি স্থানাঙ্ক মেলায়, ColorPoint তার ওপর রংও মেলায়। আলাদা আলাদা পড়লে কোনোটিতে ভুল নেই। এবার পাঁচ শর্ত একে একে পরীক্ষা।
  2. Reflexive — পাস। প্রতিটি object নিজের সমান। এই শর্ত ভাঙা কঠিন; ভাঙে সাধারণত কেউ ইচ্ছা করে অদ্ভুত কিছু লিখলে।
  3. Symmetric — ফেল। Point-এর চোখে ColorPoint একটি Point (instanceof সত্য), তাই সে রং না দেখে বলে “সমান”। ColorPoint-এর চোখে Point কোনো ColorPoint নয়, তাই বলে “সমান নয়”। একই জোড়া, দুই উত্তর।
  4. Collection-এর বিভ্রান্তি। ArrayList.contains ডাকে খোঁজা_জিনিস.equals(উপাদান). তাই কে list-এ আছে আর কে খুঁজছে, তার ওপর ফল উল্টে যায়। এই bug সাধারণত বহুদিন পরে, অন্য কারো কোডে ধরা পড়ে।
  5. সারানোর চেষ্টা আরও খারাপ। ColorPoint যদি সাধারণ Point পেলে রং বাদ দিয়ে মেলায়, symmetry ঠিক হয় — কিন্তু লাল আর নীল দুটিই সাধারণ Point-এর “সমান”, অথচ পরস্পরের নয়। Transitivity ভাঙল।
  6. বাকি দুই শর্ত পাস, কিন্তু ক্ষতি হয়ে গেছে। Bloch-এর সিদ্ধান্ত: instantiable একটি class extends করে নতুন মান-field যোগ করলে equals contract পুরোপুরি রক্ষার সাধারণ কোনো উপায় নেই (ভাবনার প্রশ্ন ২)। ব্যবহারিক সমাধান — composition, অথবা value class-কে final রাখা। Record নিজে থেকেই final (পর্ব ২৫)।

৫ · hashCode — HashSet কীভাবে এত দ্রুত খোঁজে

দশ লাখ রেজিস্ট্রেশনের একটি তালিকায় “রোল ৭, Rina আছে কি?” — ArrayList হলে প্রতিটি উপাদানের সাথে equals ডাকতে হয়, সবচেয়ে খারাপ ক্ষেত্রে দশ লাখবার। HashSet আর HashMap এই কাজটি গড়ে প্রায় এক-দুইবার তুলনায় সারে। কৌশলটি হলো hashCode(): প্রতিটি object থেকে একটি int হিসাব করা, আর সেই int দিয়ে ঠিক করা object-টি ভেতরের টেবিলের কোন ঘরে থাকবে। সেই ঘরকে বলে bucket.

পোস্ট অফিসের কথা ভাবুন। চিঠি সাজানোর কর্মী প্রথমে পোস্ট কোড দেখে চিঠিটি সঠিক খোপে ফেলেন — ১২০৫ ধানমন্ডি, ৪০০০ চট্টগ্রাম। কোনো চিঠি খুঁজতে হলে পুরো অফিস নয়, শুধু একটি খোপ ঘাঁটলেই হয়। পোস্ট কোড হলো hashCode, খোপ হলো bucket, আর খোপের ভেতরে নাম-ঠিকানা মিলিয়ে নিশ্চিত হওয়া হলো equals. ধাপে ধাপে দেখুন:

ধাপ ১set.add(new Student(7, "Rina"))
hashCode()  → একটি int        (সংখ্যাগুলো উদাহরণ মাত্র)
int থেকে ঘর → bucket 5   (১৬টি ঘরের টেবিল)

 0 │ ·
 …
 5 │ Student(7, Rina)
 …
15 │ ·
ধাপ ২আরও দুজন যোগ হলো
 2 │ Student(12, Karim)
 5 │ Student(7, Rina)  →  Student(31, Sumi)
 …
Sumi-র hashCode আলাদা, কিন্তু ঘর মিলে গেল 5-এ —
একে বলে hash collision; এটা স্বাভাবিক,
একই ঘরে একটি ছোট তালিকা হয়
ধাপ ৩set.contains(new Student(7, "Rina"))
নতুন object, কিন্তু একই roll আর name
hashCode() → একই int  → bucket 5
শুধু bucket 5 খোঁজা হবে — বাকি ১৫টি ঘর ছোঁয়াই হবে না
ধাপ ৪ঘরের ভেতরে — আগে hash, পরে equals
bucket 5:
  Student(7, Rina)  hash মেলে?  হ্যাঁ → equals()? true
  → contains = true
Sumi পর্যন্ত যেতেই হলো না
ধাপ ৫set.contains(new Student(99, "Nabil"))
hashCode() → অন্য int → bucket 11
bucket 11 খালি → contains = false
একবারও equals ডাকা হয়নি — "না" উত্তরও দ্রুত
  1. hashCode থেকে ঘর। HashSet (ভেতরে আসলে একটি HashMap) object-এর hashCode নেয়, তার বিটগুলো একটু মিশিয়ে টেবিলের আকার দিয়ে ছেঁটে একটি ঘরের নম্বর পায়। Object সেই ঘরে বসে। সংখ্যাগুলো এখানে বোঝানোর জন্য বানানো — আসল মান আপনার দেখার দরকার নেই।
  2. Collision স্বাভাবিক। int-এর মান প্রায় ৪৩০ কোটি রকম, কিন্তু টেবিলে ঘর অল্প। তাই আলাদা object একই ঘরে পড়তেই পারে — এটা hash collision, কোনো ভুল নয়। ঘরের ভেতরে একটি ছোট তালিকা তৈরি হয়।
  3. খোঁজার সময় একই হিসাব। contains-এ দেওয়া object নতুন, কিন্তু রোল আর নাম এক — তাই সঠিক hashCode লেখা থাকলে একই int, একই ঘর। পুরো set নয়, শুধু একটি ঘর দেখা হয়।
  4. ঘরের ভেতরে equals নিশ্চিত করে। HashMap আগে সংরক্ষিত hash মান মেলায় (সস্তা int তুলনা), মিললে তবেই equals ডাকে। hashCode শুধু “কোথায় খুঁজব” বলে; “এটাই কি সেই জিনিস” বলে equals.
  5. “না”-ও দ্রুত। আলাদা hashCode মানে প্রায় সবসময় আলাদা ঘর, সেখানে কেউ নেই, তাই equals ডাকারই দরকার হয় না। পুরো ব্যবস্থাটা দাঁড়িয়ে আছে একটি প্রতিশ্রুতির ওপর: সমান object-এর hashCode সমান। এই প্রতিশ্রুতি ভাঙলে কী হয়, নিচে দেখুন।

hashCode-এর চুক্তি

Object-এর Javadoc তিনটি শর্ত দেয়। প্রথম দুটিই আসল:

a.equals(b) সত্য হলে
a.hashCode() == b.hashCode() হতেই হবে।
  • সমান object → সমান hashCode. বাধ্যতামূলক। এটাই সেই প্রতিশ্রুতি।
  • উল্টোটা সত্য নয়: hashCode সমান হলেও object সমান না-ও হতে পারে (collision). "Aa" আর "BB"-এর hashCode হুবহু এক — চালিয়ে যাচাই করা যায়।
  • একই চলার মধ্যে object না বদলালে hashCode বদলাবে না। তবে প্রোগ্রাম আবার চালালে ভিন্ন হতে পারে — তাই hashCode কখনো ফাইলে বা ডেটাবেসে সংরক্ষণ করবেন না।

একটি hashCode যা সবসময় return 42; করে, চুক্তি মানে — সমান object-এর hashCode তো সমানই! কিন্তু তখন সব object একই ঘরে, আর HashSet একটি ধীর তালিকা হয়ে যায়। ভালো hashCode আলাদা object-কে যতটা সম্ভব ছড়িয়ে দেয়। নিজে হিসাব না করে লিখুন:

@Override
public int hashCode() {
    return Objects.hash(roll, name);     // equals-এ যে field, ঠিক সেই field-ই
}

Objects.hash ভেতরে প্রতিটি field-এর hashCode নিয়ে 31 * result + h সূত্রে জোড়ে — Bloch-এর বিখ্যাত রেসিপি (ভাবনার প্রশ্ন ৩)। একমাত্র দাম: varargs বলে প্রতিবার একটি ছোট array আর primitive-এর boxing হয়। খুব গরম কোডে (কোটিবার ডাকা) হাতে 31 * Integer.hashCode(roll) + name.hashCode() লেখা হয়; সাধারণ কোডে Objects.hash-ই যথেষ্ট। মূল নিয়ম: equals-এ যে field ব্যবহার করেছেন, hashCode-এ ঠিক সেগুলোই — বেশি হলে সমান object আলাদা hash পাবে, কম হলে শুধু collision বাড়বে।

hashCode না লিখলে — হাতে-কলমে

নিচের প্রোগ্রামে দুটি class. BadStudent শুধু equals override করেছে, Student দুটোই। দুটি set-এ একই ছাত্রকে দুবার রেজিস্টার করা হচ্ছে:

import java.util.*;

class BadStudent {                          // equals আছে, hashCode নেই
    private final int roll; private final String name;
    BadStudent(int roll, String name) { this.roll = roll; this.name = name; }
    @Override public boolean equals(Object o) {
        return o instanceof BadStudent s && roll == s.roll && name.equals(s.name);
    }
}
class Student {                             // equals আর hashCode দুটোই
    private final int roll; private final String name;
    Student(int roll, String name) { this.roll = roll; this.name = name; }
    @Override public boolean equals(Object o) {
        return o instanceof Student s && roll == s.roll && name.equals(s.name);
    }
    @Override public int hashCode() { return Objects.hash(roll, name); }
}
class Main {
    public static void main(String[] args) {
        Set<BadStudent> bad = new HashSet<>();
        bad.add(new BadStudent(7, "Rina"));  bad.add(new BadStudent(7, "Rina"));
        System.out.println("bad:  size=" + bad.size() + " contains=" + bad.contains(new BadStudent(7, "Rina")));
        Set<Student> good = new HashSet<>();
        good.add(new Student(7, "Rina"));  good.add(new Student(7, "Rina"));
        System.out.println("good: size=" + good.size() + " contains=" + good.contains(new Student(7, "Rina")));
    }
}

আউটপুট: bad: size=2 contains=false আর good: size=1 contains=true. BadStudent-এর equals নিখুঁত, তবু set-এ “একই” ছাত্র দুজন, আর তৃতীয় একটি সমান object দিয়ে খুঁজলে পাওয়া যায় না। কারণ BadStudent-এর hashCode Object থেকে আসা — identity hash code, যা প্রতিটি নতুন object-এর জন্য আলাদা। ফলে তিনটি object তিনটি আলাদা hash পায়, আর HashSet equals ডাকার সুযোগই পায় না। Student-এ Objects.hash(roll, name) যোগ করতেই সব ঠিক। লক্ষ করুন, প্রোগ্রামটি size আর contains ছাপে, hash মান বা set-এর ক্রম নয় — ওগুলো প্রতিবার চালালে বদলাতে পারে। কোনো compile error, warning বা exception নেই — এই bug শুধু ভুল উত্তর দিয়ে নিজেকে প্রকাশ করে।

ঠিক কোথায় ব্যাপারটা ভাঙল, BadStudent-এর দিক থেকে ধাপে ধাপে দেখুন:

1bad.add(new BadStudent(7, "Rina"));   // A2bad.add(new BadStudent(7, "Rina"));   // B3bad.contains(new BadStudent(7, "Rina")); // C4bad.size();
set-এর ভেতরে (সংখ্যা উদাহরণ মাত্র)
এখনকার object
identity hash
bucket
equals ডাকা হলো?
  1. A ঢুকল। BadStudent hashCode override করেনি, তাই Object-এর identity hash চলে — object-এর মান নয়, তার আলাদা অস্তিত্ব থেকে আসা একটি সংখ্যা। ধরা যাক সেটা A-কে ঘর 3-এ পাঠাল।
  2. B — মানে A-এর হুবহু সমান — ঢুকল অন্য ঘরে। B আলাদা object, তাই তার identity hash আলাদা, ঘরও আলাদা (11). ঘর 11 খালি, তাই HashSet ধরে নেয় B নতুন। A-এর সাথে equals মেলানোর প্রশ্নই ওঠেনি — HashSet শুধু একই ঘরের বাসিন্দাদের সাথে তুলনা করে।
  3. C দিয়ে খোঁজা। C-ও সমান, কিন্তু তার hash আবার আলাদা — সে পৌঁছায় তৃতীয় একটি ঘরে।
  4. খালি ঘর, উত্তর “নেই”। ঘর 6 খালি, তাই contains false. দুর্লভ ক্ষেত্রে C যদি A-এর ঘরেই পড়ত, তবুও HashMap আগে hash মান মেলায় — h3 আর h1 আলাদা, তাই equals ডাকার আগেই বাদ। equals যত নিখুঁতই হোক, সে কখনো সুযোগ পায় না।
  5. ফল: size 2, equals ০ বার। set-এর মূল প্রতিশ্রুতি — “একই জিনিস একবার” — ভেঙে গেল, কোনো error ছাড়া। HashMap-এ key হিসেবে BadStudent রাখলেও একই কাণ্ড: put করা মান get দিয়ে আর পাওয়া যায় না।

৬ · বদলানো যায় এমন key — HashSet-এ হারিয়ে যাওয়া object

hashCode ঠিকঠাক লিখলেও আরেকটি ফাঁদ আছে। ধরুন Student-এর name final নয়, একটি setName আছে। Rina-কে set-এ রাখার পর তার নাম সংশোধন করে করা হলো “Rina Akter”:

Set<Student> set = new HashSet<>();
Student s = new Student(7, "Rina");     // name এবার mutable, hashCode = Objects.hash(roll, name)
set.add(s);                              // পুরোনো নামের hash দিয়ে একটি ঘরে বসল
s.setName("Rina Akter");                 // hash বদলে গেল, কিন্তু object আগের ঘরেই রইল
System.out.println(set.contains(s));     // false — একই object, তবু "নেই"!
set.add(s);
System.out.println(set.size());          // 2 — একই object দুবার

OpenJDK 21-এ চালিয়ে দেখা ফল: false, তারপর 2. object-টি এখনো set-এর ভেতরেই আছে, কিন্তু পুরোনো নামের ঘরে। নতুন নামের hash অন্য ঘর দেখায়, সেখানে কেউ নেই। আর আবার add করলে একই object দ্বিতীয় ঘরে ঢোকে। remove(s)-ও ব্যর্থ হয় — object-টি কার্যত set-এর ভেতরে আটকে গেল, একটি ছোট memory leak.

HashSet-এর উপাদান বা HashMap-এর key হিসেবে রাখা object-এর equals/hashCode-এ যে field আছে, তা কখনো বদলাবেন না। সবচেয়ে নিরাপদ উপায়: value object-কে immutable করুন — সব field private final. String আর Integer সবচেয়ে প্রচলিত key, কারণ এরা immutable. যদি object-কে mutable রাখতেই হয়, তাহলে equals/hashCode শুধু না-বদলানো পরিচয়-field (যেমন roll বা id) দিয়ে লিখুন।

এখান থেকে equals-এর জন্য field বাছাইয়ের একটি সাধারণ নিয়মও বেরিয়ে আসে। লাইব্রেরির একটি Book-এ আছে isbn, title, copiesAvailable আর lastBorrowedAt. শেষ দুটি প্রতিদিন বদলায় — বই ধার দেওয়া হলেই। এগুলো বইয়ের পরিচয় নয়, অবস্থা। এগুলো equals-এ রাখলে একই বই সকালে আর বিকেলে “আলাদা” হয়ে যাবে। আর যে field অন্য field থেকে হিসাব করা যায় (যেমন totalPrice = price * qty), সেটাও রাখার দরকার নেই — বাড়তি কাজ, আর অমিলের ঝুঁকি। এখানে পরিচয় শুধু isbn.

C/C++/Python জানলে

C-তে struct-এর জন্য এমন কোনো ব্যবস্থা নেই — তুলনা আর hash-এর function নিজে লিখে hash table-এ পাঠাতে হয়। C++-এ operator== overload করেন, আর unordered_set-এর জন্য আলাদা std::hash specialization লাগে; C++20-এ operator<=> = default করে তুলনা বানানো যায় (C++-এর operator overloading). Python-এ __eq__, __hash__, __repr__ আর __lt__ — আর একটি গুরুত্বপূর্ণ পার্থক্য: Python-এ __eq__ লিখে __hash__ না লিখলে object-টি unhashable হয়ে যায়, set-এ রাখতে গেলে সাথে সাথে TypeError. Java একই ভুলে নীরবে ভুল উত্তর দেয় (Python-এর dunder method).

৭ · Comparable আর compareTo — স্বাভাবিক ক্রম

equals বলে দুটি জিনিস সমান কি না। কিন্তু অনলাইন দোকানে পণ্য “সস্তা থেকে দামি” সাজাতে জানতে হয় কোনটি আগে। Java-তে একটি class নিজের এই natural ordering ঘোষণা করে Comparable<T> interface implement করে। এর একটিই method — compareTo:

a.compareTo(b) ফেরত দেয়মানে
ঋণাত্মক যেকোনো সংখ্যাa আগে, b পরে (a “ছোট”)
শূন্যক্রমের দিক থেকে সমান
ধনাত্মক যেকোনো সংখ্যাa পরে (a “বড়”)

খেয়াল করুন, শুধু চিহ্ন গুরুত্বপূর্ণ — −১ আর −৫০০-এর মানে একই। এরও চুক্তি আছে, equals-এর মতোই: উল্টালে চিহ্ন উল্টাবে (a < b হলে b > a), transitive (a < b আর b < c হলে a < c), আর শূন্য পাওয়া দুটি object অন্য সবার সাথে একইভাবে তুলনীয়। আর একটি জোরালো সুপারিশ: compareTo শূন্য দিলে equals-ও true দেওয়া উচিত — একে বলে “consistent with equals”.

কেন? HashSet সমান কি না দেখে equals দিয়ে, কিন্তু TreeSet দেখে compareTo দিয়ে। দুটি অমিল হলে একই ডেটা দুই set-এ আলাদা আচরণ করে। JDK-র নিজের উদাহরণ BigDecimal: 4.0 আর 4.00-এর compareTo শূন্য (সংখ্যা হিসেবে সমান), কিন্তু equals false (scale আলাদা)। ফলে দুটিকে HashSet-এ রাখলে size 2, TreeSet-এ রাখলে size 1 — OpenJDK 21-এ চালিয়ে যাচাই করা। Comparable-এর Javadoc নিজেই BigDecimal-কে এই ব্যতিক্রমের উদাহরণ হিসেবে দেখায়।

দামের ক্রমে Product — আর বিয়োগের bug

import java.util.*;

class Product implements Comparable<Product> {
    private final String name;
    private final int price;                       // টাকায়
    Product(String name, int price) { this.name = name; this.price = price; }
    @Override public int compareTo(Product other) {
        return Integer.compare(price, other.price);  // বিয়োগ নয়
    }
    @Override public String toString() { return name + "=" + price; }
}
class Main {
    public static void main(String[] args) {
        Product[] cart = { new Product("Bag", 1500), new Product("Pen", 20),
                           new Product("Book", 350), new Product("Lamp", 900) };
        Arrays.sort(cart);                            // compareTo দিয়ে সাজায়
        System.out.println(Arrays.toString(cart));

        int a = -2_000_000_000, b = 2_000_000_000;    // a আসলে b-এর চেয়ে ছোট
        System.out.println("a - b = " + (a - b));
        System.out.println("Integer.compare(a, b) = " + Integer.compare(a, b));
    }
}

আউটপুট: [Pen=20, Book=350, Lamp=900, Bag=1500], তারপর a - b = 294967296 আর Integer.compare(a, b) = -1. Arrays.sort(cart) কোনো তুলনার নিয়ম চায়নি, কারণ Product নিজেই Comparable — sort প্রতিটি জোড়ায় compareTo ডেকেছে। দ্বিতীয় অংশটিই এই পর্বের সতর্কবার্তা: −২০০ কোটি থেকে +২০০ কোটি বিয়োগ করলে আসল উত্তর −৪০০ কোটি, যা int-এ ধরে না (int-এর সীমা প্রায় ±২১৪ কোটি)। Integer overflow-এ মান ঘুরে আসে একটি ধনাত্মক সংখ্যায় — অর্থাৎ বিয়োগ দিয়ে লেখা compareTo বলত “a বড়”, যা সম্পূর্ণ উল্টো। Integer.compare বিয়োগ করে না, সরাসরি তুলনা করে, তাই সবসময় সঠিক চিহ্ন দেয়।

“দাম তো কখনো ২০০ কোটি হবে না” — সত্যি, Product-এর দাম হয়তো কখনো overflow করবে না। কিন্তু একই অভ্যাস একদিন ব্যালান্স, timestamp, ঋণাত্মক তাপমাত্রা বা hash মান তুলনায় চলে যায়। সবচেয়ে চরম ক্ষেত্রটি ধাপে ধাপে দেখুন:

1int a = Integer.MIN_VALUE;  // -21474836482int b = 1;3int bad = a - b;            // compareTo-র ভুল ধরন4int good = Integer.compare(a, b);
ভ্যারিয়েবল
a
b
গণিতে a - b
bad
good
  1. দুটি সংখ্যা। a হলো int-এর সবচেয়ে ছোট মান, −2147483648. স্পষ্টতই a, b-এর চেয়ে ছোট, তাই compareTo-র উত্তর ঋণাত্মক হওয়া উচিত।
  2. গণিতের উত্তর ধরে না। a − b = −2147483649 — int-এর সর্বনিম্ন সীমার চেয়েও এক কম। ৩২ বিটে এই সংখ্যার জায়গা নেই।
  3. Overflow — চিহ্ন উল্টে গেল। Java exception দেয় না; two's complement নিয়মে মান ঘুরে আসে সবচেয়ে বড় ধনাত্মক int-এ, 2147483647 (পর্ব ০৭)। sort এখন ভাববে a সবার শেষে — সবচেয়ে ছোট সংখ্যা সবচেয়ে বড় হয়ে গেল। ডেটায় এমন মান কদাচিৎ থাকে, তাই bug মাসের পর মাস লুকিয়ে থাকে।
  4. Integer.compare — কোনো বিয়োগ নেই। এটি ভেতরে লেখে (x < y) ? -1 : ((x == y) ? 0 : 1), তাই overflow অসম্ভব। একইভাবে আছে Long.compare, Double.compare. বিয়োগের “চালাকি” এসেছে C-এর পুরোনো অভ্যাস থেকে; Java-তে এর দরকার নেই।

Comparable না থাকলে, আর একাধিক ক্রম দরকার হলে

যে class Comparable নয়, তার array Arrays.sort-এ দিলে compile হয়ে যায় (method-টি Object[] নেয়), কিন্তু চলার সময় ভেঙে পড়ে:

Exception in thread "main" java.lang.ClassCastException: class Book cannot be cast to class java.lang.Comparable (Book is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')

আর Comparable implement করে compareTo লিখতে ভুলে গেলে javac ধরে:

Main.java:1: error: Product is not abstract and does not override abstract method compareTo(Product) in Comparable
class Product implements Comparable<Product> {
^
1 error

একটি class-এর natural ordering একটিই হয় — Product-এর ক্ষেত্রে দাম। কিন্তু একই পণ্য কখনো নামের ক্রমে, কখনো রেটিং-এর ক্রমে সাজাতে হয়। তার জন্য class-এর বাইরে থেকে নিয়ম দেওয়া হয় Comparator দিয়ে, যেমন Arrays.sort(cart, Comparator.comparing(Product::name)) — পর্ব ৩৩-এর পুরো বিষয়। আপাতত মনে রাখুন: Comparable = “আমি নিজে কীভাবে সাজি”, Comparator = “বাইরে থেকে বলা অন্য একটি ক্রম”।

৮ · নিজে না লিখে — IDE, record, Lombok; আর clone কেন নয়

এত নিয়ম জেনে মনে হতে পারে প্রতিটি class-এ ৩০ লাইন হাতে লিখতে হবে। বাস্তবে পেশাদাররা প্রায় কখনো হাতে লেখেন না। তিনটি উপায় আছে, পুরোনো থেকে নতুন:

উপায়কীভাবেসতর্কতা
IDE-র generateIntelliJ IDEA-য় Alt+Insert (macOS-এ ⌘N) → equals() and hashCode(); Eclipse-এ Source মেনু। কোন field নেবেন, বেছে দেন।নতুন field যোগ করলে আবার generate করতে হয় — নইলে পুরোনো কোড নতুন field দেখে না
record (Java 16 থেকে)record Money(long poisha, String currency) {} — equals, hashCode, toString নিজে থেকে, সব component দিয়েRecord final আর field final; সব component পরিচয়ে ঢোকে। পর্ব ২৫-এ বিস্তারিত
Lombok@EqualsAndHashCode, @ToString বা @Data annotation — compile-এর সময় কোড তৈরিJDK-র অংশ নয়; @Data setter-ও বানায়, তাই mutable key-এর ফাঁদ (অংশ ৬) সহজে তৈরি হয়

নিয়ম না জানলে টুল-ও বিপদ: IDE-তে সব field-এ টিক দিলে copiesAvailable-এর মতো বদলানো field-ও equals-এ ঢুকবে। টুল কোড লেখে, কোন field পরিচয় — সেই সিদ্ধান্ত আপনার।

clone() — কেন এড়ানো হয়

Object-এর clone() দেখে মনে হয় কপি বানানোর সহজ পথ। কিন্তু এর নকশা অদ্ভুত: Cloneable interface-এ কোনো method নেই, শুধু একটি “অনুমতির চিহ্ন”; না থাকলে clone চলার সময় CloneNotSupportedException দেয়। clone constructor না ডেকেই object বানায়, তাই final field নতুন করে বসানো যায় না। আর ডিফল্টে shallow copy করে — ভেতরের list বা array দুই কপির মধ্যে ভাগাভাগি থাকে। Bloch নিজে বলেছেন Cloneable “broken”. পর্ব ১৬-এ দেখা copy constructor (new Student(other)) বা একটি static factory (Student.copyOf(other)) প্রায় সবসময় ভালো: স্পষ্ট, final-বান্ধব, আর আপনি নিজে ঠিক করেন কোনটা গভীর কপি হবে। (আর finalize()? সেটা Java 9 থেকে deprecated — ব্যবহার করবেন না; resource বন্ধ করতে try-with-resources, পর্ব ২৯।)

পরীক্ষা ও ইন্টারভিউতে
  • == আর equals()-এর পার্থক্য? — == দেখে দুটি reference একই object কি না; equals দেখে মান সমান কি না (যদি class override করে থাকে, নইলে সেটাও ==)।
  • equals override করলে hashCode কেন? — চুক্তি: সমান object-এর hashCode সমান হতেই হবে; না হলে HashMap/HashSet ভুল ঘরে খোঁজে, ডুপ্লিকেট ঢোকে আর get/contains ব্যর্থ হয়।
  • hashCode সমান হলে কি equals true? — না, collision হতে পারে (যেমন "Aa" আর "BB"). উল্টোটা বাধ্যতামূলক, এটা নয়।
  • equals contract-এর শর্তগুলো? — reflexive, symmetric, transitive, consistent, আর x.equals(null) false.
  • Comparable আর Comparator-এর পার্থক্য? — Comparable class-এর ভেতরে একটি স্বাভাবিক ক্রম (compareTo); Comparator বাইরে থেকে দেওয়া যত খুশি ক্রম (compare).

৯ · AI-এর যুগে — চুক্তি যাচাই করা আপনার কাজ

AI-কে “একটি Student class লিখে দাও” বললে সাধারণত সুন্দর দেখতে কোড আসে। কিন্তু এই পর্বের bug-গুলো ঠিক সেই ধরনের যা compile হয়, চলে, আর ছোট টেস্টে ধরা পড়ে না — তাই AI-এর আউটপুটে এগুলো প্রায়ই টিকে থাকে। AI শিখেছে লাখো পুরোনো টিউটোরিয়াল থেকে, যার অনেকগুলোতে বিয়োগ দিয়ে compareTo বা hashCode ছাড়া equals আছে।

AI যা করেকেন সমস্যাআপনি যা করবেন
equals লেখে, hashCode বাদ দেয়HashSet/HashMap-এ ডুপ্লিকেট, contains/get ব্যর্থদুটি সবসময় একসাথে, একই field দিয়ে — শর্ত দিন
compareTo-তে return this.x - other.x;বড় বা ঋণাত্মক মানে overflow, sort-এর ক্রম উল্টে যায়Integer.compare / Long.compare চান
সব field দিয়ে equals — বদলানো আর হিসাব-করা field-সহএকই জিনিস সময়ের সাথে “আলাদা” হয়; HashSet-এ object হারায়কোন field পরিচয়, আগে নিজে ঠিক করে বলে দিন
equals(Student s) বা @Override বাদoverride নয়, overload; collection Object-এর equals চালায়প্রতিটিতে @Override, parameter Object
non-final class-এ instanceof equals, পরে subclass-এ নতুন fieldsymmetry ভাঙে (Point/ColorPoint)value class final বা record করতে বলুন
toString-এ পাসওয়ার্ড বা টোকেনগোপন তথ্য log-এ চলে যায়কোন field toString-এ বাদ, স্পষ্ট বলুন

AI-কে value class লিখতে বা রিভিউ করতে বলার সময় এই কাঠামো ব্যবহার করুন:

আমি Java 21 শিখছি, equals/hashCode/compareTo-এর চুক্তি বুঝি। নিচের class-এর জন্য equals, hashCode, toString আর (দরকার হলে) compareTo লিখে দাও: [যেমন — Student(roll, name, phone), যেটা পরীক্ষার রেজিস্ট্রেশনের HashSet-এ থাকবে] শর্ত: ১. পরিচয় নির্ধারণ করবে শুধু এই field-গুলো: [যেমন — roll]. বাকিগুলো equals/hashCode-এ দেবে না, আর কেন দিলে না এক লাইনে বলো। ২. equals আর hashCode সবসময় একসাথে, ঠিক একই field দিয়ে; hashCode-এ Objects.hash ব্যবহার করো। ৩. equals(Object o) — parameter Object; প্রতিটি override-এ @Override. ৪. null হতে পারে এমন field মেলাতে Objects.equals ব্যবহার করো। ৫. compareTo-তে কখনো বিয়োগ নয় — Integer.compare / Long.compare; আর compareTo শূন্য দিলে equals true হবে কি না বলো। ৬. পরিচয়ের field private final রাখো; class extends করার জন্য না হলে final করো, অথবা record ভালো হলে record দাও। ৭. toString-এ কোনো গোপন তথ্য (পাসওয়ার্ড, PIN, OTP) রাখবে না। ৮. শেষে একটি ছোট main দাও যা HashSet-এ দুটি সমান object রেখে size আর contains ছাপে — hashCode-এর মান বা ডিফল্ট toString ছাপবে না।

AI-এর লেখা equals/hashCode হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১equals আর hashCode প্রতিটি object-এ রাখা — Java-র এই নকশা কি ঠিক ছিল? অন্য ভাষাগুলো কী করেছে?

১৯৯৬ সালের JDK 1.0-এ generics ছিল না। Collection বলতে ছিল Vector আর Hashtable, যারা যেকোনো object রাখত — টাইপ Object. Hashtable-কে যেকোনো key-এর hash জানতে হবে, Vector-কে যেকোনো উপাদান খুঁজতে হবে। Generics বা আলাদা interface ছাড়া সবচেয়ে সহজ সমাধান ছিল: সব object-এর মূলে, Object class-এ, equals আর hashCode বসিয়ে দেওয়া। তখনকার জন্য এটা ব্যবহারিক ছিল — যেকোনো object আজও যেকোনো HashMap-এর key হতে পারে।

দাম কী। প্রথমত, প্রতিটি class একটি equality নিয়ে জন্মায়, যা প্রায়ই অর্থহীন — দুটি Thread বা দুটি Socket “সমান” কি না, এই প্রশ্নই অদ্ভুত। দ্বিতীয়ত, ডিফল্টটি identity হওয়ায় value class-এ override ভুলে গেলে কোনো error আসে না, শুধু ভুল উত্তর — এই পর্বের প্রতিটি bug এখান থেকে। তৃতীয়ত, equals(Object) যেকোনো টাইপ নেয়, তাই "5".equals(5) লিখলে compiler কিছু বলে না, শুধু false আসে। চতুর্থত, array পর্যন্ত Object-এর identity equals পায়, তাই আলাদা Arrays.equals লাগে।

Bloch-এর নিয়ম। Joshua Bloch — যিনি Sun-এ Java Collections Framework নকশা করেছিলেন — Effective Java-র তৃতীয় সংস্করণে (২০১৮) Item 10 লিখেছেন “Obey the general contract when overriding equals” আর Item 11 “Always override hashCode when you override equals”. লক্ষ করুন, এগুলো ভাষার নিয়ম নয়, সতর্কবাণী — ভাষা আপনাকে বাধা দেয় না, বই সতর্ক করে। একটি ভালো নকশা হলে compiler নিজেই এই ভুল ধরত।

অন্যরা কী করেছে। Haskell-এ সমতা একটি type class (Eq) — যে টাইপ Eq নয়, তাকে == দিয়ে তুলনা করাই যায় না, compile error. Rust-এ PartialEq, Eq আর Hash আলাদা trait, আর #[derive(PartialEq, Eq, Hash)] দিয়ে একসাথে তৈরি হয়; HashMap-এর key হতে হলে Eq + Hash লাগবেই। C#-এ Java-র মতোই Object-এ Equals/GetHashCode আছে, পাশে IEquatable<T>. Kotlin-এর data class আর Scala-র case class তৈরি করে দেয়। Java নিজেও এই দিকে এসেছে: Java 16-এর record ঠিক এই সমস্যার উত্তর — আর Guava-র মতো library-তে Equivalence নামে বাইরে থেকে দেওয়া সমতার ধারণাও আছে।

মূল উপলব্ধি: Object-এ equals/hashCode রাখা ছিল generics-হীন যুগের ব্যবহারিক সমাধান, যার দাম আজও দিতে হয় নীরব bug দিয়ে। আধুনিক ভাষাগুলো সমতাকে ঐচ্ছিক, টাইপ-নিরাপদ আর একসাথে তৈরি করা একটি ক্ষমতা বানিয়েছে — Java-তে সেই ভূমিকা নেয় record আর আপনার শৃঙ্খলা।

প্র ০২Bloch বলেছেন, instantiable class extends করে নতুন মান-field যোগ করলে equals contract রক্ষার উপায় নেই। কথাটার মানে কী, আর JDK নিজে কোথায় এই ভুল করেছে?

Bloch-এর বাক্যটি প্রায় হুবহু: instantiable একটি class extends করে একটি value component যোগ করে equals contract রক্ষার কোনো উপায় নেই — যদি না আপনি object-oriented abstraction-এর সুবিধা ছাড়তে রাজি থাকেন। অংশ ৪-এর stepper-এ দুটি চেষ্টা দেখেছেন: instanceof দিয়ে লিখলে symmetry ভাঙে, আর “Point পেলে রং বাদ দাও” লিখলে transitivity ভাঙে।

getClass দিয়ে লিখলে? তৃতীয় চেষ্টা: Point-এর equals-এ getClass() != o.getClass(). তাহলে Point আর ColorPoint কখনো সমান নয় — symmetric আর transitive দুটোই রক্ষা হয়। কিন্তু দাম দিতে হয় Liskov substitution principle-এ (পর্ব ১৯)। ধরুন কেউ CounterPoint extends Point বানালেন, যা কোনো নতুন মান-field যোগ করে না, শুধু কতগুলো তৈরি হলো গোনে। এখন একটি “একক বৃত্তের বিন্দুগুলো”র HashSet-এ new CounterPoint(1, 0) দিয়ে খুঁজলে পাওয়া যায় না, যদিও সেটা আসলে (1, 0) বিন্দু। subclass-কে parent-এর জায়গায় বসানো যাচ্ছে না — inheritance-এর মূল প্রতিশ্রুতি ভাঙল। Bloch তাই getClass পদ্ধতির বিরোধী।

JDK-র নিজের উদাহরণ। java.sql.Timestamp extends java.util.Date, আর তার সাথে যোগ করেছে nanosecond-এর একটি field. ফল: একটি Date আর একটি Timestamp-এর মধ্যে equals symmetric নয়। Timestamp-এর Javadoc নিজেই স্বীকার করে যে তার equals, Date-এর equals-এর সাথে symmetric নয়, আর সতর্ক করে যে দুটিকে একই collection-এ মেশালে অদ্ভুত আচরণ হতে পারে। এটা ঠিক করা যায় না, কারণ “Timestamp is a Date” একটি public, স্থায়ী ঘোষণা (পর্ব ১৯-এর Stack extends Vector-এর মতো)।

বিকল্প পথ। ২০০৯ সালে Martin Odersky, Lex Spoon আর Bill Venners Artima-তে “How to Write an Equality Method in Java” প্রবন্ধে একটি canEqual method-এর কৌশল দেখান: প্রতিটি class জানায় সে কার সাথে সমান হতে রাজি, আর subclass তা override করে। এটা কাজ করে, কিন্তু প্রতিটি class-এ বাড়তি নিয়ম মানতে হয়; Scala-র case class এই ধারণার কাছাকাছি। সবচেয়ে সহজ ও প্রচলিত পথ এখনো দুটি: composition — ColorPoint-এর ভেতরে একটি Point field আর একটি asPoint() method — অথবা value class-কে final রাখা, যেটা record নিজেই করে।

মূল উপলব্ধি: সমতা একটি “দুই পক্ষের” চুক্তি, আর inheritance এক পক্ষকে গোপনে বদলে দিতে দেয়। তাই value object-এর জন্য inheritance প্রায় সবসময় ভুল হাতিয়ার — final class, record বা composition নিন।

প্র ০৩Objects.hash আর String.hashCode দুটোই 31 দিয়ে গুণ করে। এই সংখ্যাটি কেন, আর Bloch-এর hashCode রেসিপি কোথা থেকে এল?

রেসিপি। Effective Java-র hashCode রেসিপিটি মোটামুটি এরকম: একটি int result নিন, প্রথম গুরুত্বপূর্ণ field-এর hash দিয়ে শুরু করুন; তারপর প্রতিটি পরের field-এর জন্য result = 31 * result + c, যেখানে c হলো সেই field-এর hash (primitive হলে Integer.hashCode, Double.hashCode; reference হলে তার hashCode, null হলে 0). Objects.hash (Java 7) আর Arrays.hashCode একই সূত্র ব্যবহার করে, শুধু 1 দিয়ে শুরু করে — এই পর্বের পরীক্ষায় Objects.hash(7, "Rina") আর হাতে হিসাব করা 31 * (31 * 1 + 7) + "Rina".hashCode() হুবহু এক পাওয়া গেছে।

গুণ কেন, যোগ কেন নয়? শুধু যোগ করলে ক্রম হারিয়ে যায়: Point(1, 2) আর Point(2, 1)-এর hash এক হতো। প্রতিবার 31 দিয়ে গুণ করলে প্রতিটি field-এর অবস্থান ফলাফলে আলাদা ওজন পায় — আসলে এটা 31-কে ভিত্তি ধরে একটি বহুপদী।

31 কেন? Bloch-এর ব্যাখ্যা: সংখ্যাটি বিজোড় — জোড় হলে গুণ করে overflow-এ নিচের বিট হারাত, কারণ 2 দিয়ে গুণ মানে বিট বাঁয়ে সরানো। আর এটি মৌলিক সংখ্যা; তিনি নিজেই স্বীকার করেছেন মৌলিক হওয়ার সুবিধা “কম স্পষ্ট, এটা প্রথাগত”। ব্যবহারিক সুবিধা: 31 * i লেখা যায় (i << 5) - i — একটি shift আর একটি বিয়োগ; আধুনিক JVM এই রূপান্তর নিজেই করে। প্রথাটি পুরোনো: Kernighan আর Ritchie-র The C Programming Language বইয়ের hash function-এও 31 দিয়ে গুণ আছে।

একটি শিক্ষণীয় ইতিহাস। Java 1.2-এর আগে String.hashCode লম্বা String-এর সব অক্ষর দেখত না — দ্রুততার জন্য মাত্র কয়েকটি অক্ষর নমুনা হিসেবে নিত (Bloch লিখেছেন, সর্বোচ্চ ষোলোটি)। ফল হলো ভয়াবহ: URL-এর মতো লম্বা, শুরুতে প্রায় একই String-গুলো একই hash পেত, আর HashMap কার্যত তালিকা হয়ে যেত। Java 1.2-তে পুরো String-এর ওপর 31-এর সূত্র আসে। Bloch এই ঘটনা দিয়েই সতর্ক করেন: দ্রুততার জন্য গুরুত্বপূর্ণ field hash থেকে বাদ দেবেন না।

মূল উপলব্ধি: ভালো hashCode-এর লক্ষ্য দুটি — সমান object-এর জন্য সমান মান, আর আলাদা object-কে যতটা সম্ভব ছড়িয়ে দেওয়া। 31 কোনো জাদু নয়; এটা সস্তা, বিজোড় আর পরীক্ষিত একটি প্রথা, আর আসল শিক্ষা হলো সব গুরুত্বপূর্ণ field ক্রমসহ মেশানো।

প্র ০৪String.hashCode-এর সূত্র সবার জানা। আক্রমণকারী কি এটা দিয়ে একটি Java সার্ভার অচল করে দিতে পারে? Java 8 কীভাবে নিজেকে রক্ষা করল?

সূত্র। String-এর Javadoc-এ লেখা আছে: s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1], int-এর হিসাবে। সূত্রটি প্রকাশ্য আর নির্দিষ্ট, তাই collision বানানো খুব সহজ। "Aa" আর "BB"-এর hash সমান (দুটিই 2112) — 'A' (65) × 31 + 'a' (97) = 2112, আর 'B' (66) × 31 + 'B' (66) = 2112. এখন এ দুটি জোড়া জুড়ে "AaAa", "AaBB", "BBAa", "BBBB" — চারটিরই hash এক। দশটি জোড়া জুড়লে ১০২৪টি, বিশটি জুড়লে দশ লাখের বেশি String, সবার hash হুবহু এক।

আক্রমণ। ২০১১ সালের ডিসেম্বরে জার্মানির 28C3 সম্মেলনে Alexander Klink আর Julian Wälde দেখান, প্রায় সব জনপ্রিয় ওয়েব প্ল্যাটফর্ম — PHP, Java (Tomcat-সহ), Python, Ruby, ASP.NET — এভাবে আক্রান্ত হতে পারে। ওয়েব সার্ভার একটি POST request-এর parameter-গুলো সাধারণত একটি HashMap-এ রাখে। আক্রমণকারী হাজার হাজার parameter পাঠায়, যাদের নাম সব একই hash-এর। সব একই bucket-এ পড়ে, প্রতিটি নতুন key ঢোকাতে আগের সবার সাথে equals — মোট কাজ বর্গাকারে বাড়ে। একটি ছোট request সার্ভারের CPU মিনিটের পর মিনিট আটকে রাখতে পারত। একে বলে hash flooding.

প্রতিরক্ষা। তাৎক্ষণিক সমাধান ছিল সার্ভারে parameter-এর সংখ্যা সীমিত করা (Tomcat-এ এমন একটি সীমা যোগ হয়)। Java 7-এর একটি আপডেটে String key-এর জন্য বিকল্প hashing যোগ হয়েছিল, পরে সরিয়ে নেওয়া হয়। স্থায়ী সমাধান এল Java 8-এ, JEP 180: HashMap-এর কোনো bucket-এ উপাদান বেশি হয়ে গেলে (বর্তমান কোডে ৮টির বেশি, আর টেবিল যথেষ্ট বড় হলে) সেই bucket-এর তালিকাটি একটি balanced tree (red-black tree)-তে রূপান্তরিত হয়। তখন সবচেয়ে খারাপ খোঁজাও তালিকার মতো n ধাপ নয়, log n ধাপ। key Comparable হলে (String তো বটেই) tree সেই ক্রম ব্যবহার করে আরও দক্ষ হয়।

Java কেন hash-কে random করল না? Python ৩.৩ থেকে String hash প্রতিবার চালানোর সময় random বীজ দিয়ে হিসাব করে, আর ৩.৪ থেকে SipHash ব্যবহার করে (PEP 456). Java পারেনি, কারণ String.hashCode-এর সূত্র Javadoc-এ প্রতিশ্রুত, আর কোড তার ওপর নির্ভর করে — যেমন Java 7-এর switch-এ String লিখলে compiler আগেই প্রতিটি case-এর hashCode হিসাব করে bytecode-এ বসায়। সূত্র বদলালে সেই সব compile করা কোড ভাঙত। তাই Java সমস্যাটি সমাধান করল hash নয়, HashMap বদলে।

মূল উপলব্ধি: hash table-এর গড় দ্রুততা একটি অনুমানের ওপর দাঁড়িয়ে — key-গুলো ছড়িয়ে থাকবে। শত্রু যখন key বাছে, সেই অনুমান ভাঙে; তাই নিরাপত্তা-সংবেদনশীল কাঠামোকে সবচেয়ে খারাপ ক্ষেত্রেও সহনীয় থাকতে হয়।

প্র ০৫ডিফল্ট hashCode কি object-এর মেমরি ঠিকানা? না হলে সংখ্যাটি আসে কোথা থেকে, আর থাকে কোথায়?

ঠিকানা নয়। পুরোনো Javadoc-এ লেখা থাকত, ডিফল্ট hashCode “সাধারণত” object-এর অভ্যন্তরীণ ঠিকানাকে int-এ রূপান্তর করে তৈরি হয় — এই বাক্য থেকেই “hashCode = ঠিকানা” ধারণাটি ছড়িয়েছে। কিন্তু আধুনিক JVM-এ এটা অসম্ভব। পর্ব ০৫-এ দেখেছেন, garbage collector object-দের heap-এর এক জায়গা থেকে আরেক জায়গায় সরায় (compaction). ঠিকানা বদলালে hashCode-ও বদলাত, আর চুক্তি ভাঙত — “object না বদলালে hashCode বদলাবে না”, অথচ HashMap-এ রাখা object হারিয়ে যেত।

তাহলে কী? HotSpot JVM-এ identity hash code তৈরি হয় অলসভাবে — প্রথমবার কেউ hashCode() (override না করা) বা System.identityHashCode ডাকলে। JDK 8 থেকে ডিফল্টভাবে এটি আসে প্রতিটি thread-এর নিজস্ব একটি pseudo-random সংখ্যা জেনারেটর (Marsaglia-র xor-shift ধরনের) থেকে। একবার তৈরি হলে সংখ্যাটি object-এর সাথেই সংরক্ষিত থাকে, তাই object যতবার সরুক, hash একই থাকে। এজন্যই সংখ্যাটি প্রতিবার প্রোগ্রাম চালালে আলাদা, আর এজন্যই এই কোর্সের কোড কখনো hashCode বা ডিফল্ট toString ছাপে না।

কোথায় থাকে। প্রতিটি Java object-এর শুরুতে একটি object header থাকে, যা আপনার field-এর আগে বসে। HotSpot-এ এর প্রথম অংশ “mark word” — 64-বিট JVM-এ ৮ বাইট — যেখানে GC-র বয়সের হিসাব, lock-এর তথ্য, আর হ্যাঁ, identity hash (৩১ বিট) জায়গা ভাগাভাগি করে। তারপর থাকে class-এর তথ্যের দিকে একটি pointer. ফলে একটি খালি new Object()-ও সাধারণত ১৬ বাইট নেয়। Lock আর hash একই জায়গা চাওয়ায় অতীতে একটি সূক্ষ্ম যোগসূত্র ছিল: biased locking চালু থাকা object-এ identity hash চাইলে সেই bias বাতিল হতো। (Biased locking JDK 15-এ ডিফল্টভাবে বন্ধ করে deprecated করা হয়, JEP 374.)

ভবিষ্যৎ। OpenJDK-র Project Lilliput header ছোট করার কাজ করছে; Java 24-এ পরীক্ষামূলক Compact Object Headers (JEP 450) এসেছে, যেখানে header ৮ বাইটে নামে — class-এর তথ্য, identity hash আর GC-র তথ্য একই ৬৪ বিটে আঁটানো হয়। Header আরও ছোট করার পরের ধাপে সবচেয়ে কঠিন সমস্যাগুলোর একটি identity hash-এর জায়গা; সেখানে hash করা object সরানোর সময় দরকার হলে তার পাশে বাড়তি জায়গায় hash রাখার কৌশল নিয়ে কাজ চলছে। heap, header আর GC-র পুরো ছবি পর্ব ৪৪-এ।

মূল উপলব্ধি: ডিফল্ট hashCode একটি পরিচয়-সংখ্যা, ঠিকানা নয় — একবার দেওয়া হয়, object-এর header-এ লেখা থাকে, আর GC যতই সরাক বদলায় না। সংখ্যাটির কোনো মানে নেই, শুধু স্থিরতা আছে; তাই একে কখনো ছাপবেন না, সংরক্ষণ করবেন না, বা এর ওপর যুক্তি দাঁড় করাবেন না।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. hashCode মুছে দেখুন: অংশ ৫-এর চালানো যায় এমন প্রোগ্রামে Student থেকে hashCode-এর লাইনটি মুছে দিন। না চালিয়ে বলুন দ্বিতীয় লাইনে কী ছাপবে। তারপর উল্টো পরীক্ষা: hashCode রাখুন কিন্তু equals মুছে দিন — এবার কী হবে, আর কেন?

    hashCode মুছলে Student-ও BadStudent-এর মতো হয়ে যায়: good: size=2 contains=false. উল্টো ক্ষেত্রে — hashCode আছে, equals নেই — তিনটি object একই hash পায়, একই ঘরে যায়, hash-ও মেলে; কিন্তু তারপর Object-এর equals ডাকা হয়, যা == করে, আর তিনটি আলাদা object. তাই এবারও size 2, contains false. শিক্ষা: HashSet-এর জন্য দুটিই লাগে — hashCode সঠিক ঘরে নিয়ে যায়, equals সেখানে মিল নিশ্চিত করে; একটি ছাড়া অন্যটি অকেজো।

  2. বিয়োগের compareTo ভাঙুন: অংশ ৭-এর প্রোগ্রামে compareTo-কে বদলে লিখুন return price - other.price;. এখনকার চারটি পণ্যে ফল বদলায় কি? এবার একটি পণ্যের দাম দিন Integer.MIN_VALUE (ধরুন একটি “ডিসকাউন্ট কুপন”, যার মান ঋণাত্মক) — sort-এর ফল কী হতে পারে?

    চারটি সাধারণ দামে ফল একই: [Pen=20, Book=350, Lamp=900, Bag=1500] — কারণ ছোট সংখ্যার বিয়োগে overflow হয় না। এটাই বিপদ: টেস্ট পাস করে। এবার Coupon-এর দাম Integer.MIN_VALUE দিলে MIN_VALUE - 20 overflow করে একটি বড় ধনাত্মক সংখ্যা হয়, তাই compareTo বলে কুপনটি Pen-এর চেয়ে “বড়” — অন্য সব পণ্যের ক্ষেত্রেও একই। OpenJDK 21-এ চালিয়ে দেখা ফল: [Pen=20, Book=350, Lamp=900, Bag=1500, Coupon=-2147483648] — সবচেয়ে ছোট মানটি সবার শেষে! কোনো exception নেই, শুধু ভুল ক্রম। Integer.compare-এ ফিরিয়ে দিলে কুপন সঠিকভাবে সবার আগে আসে। (মানগুলো আরও এলোমেলো হলে বিয়োগের compareTo চুক্তির transitivity-ও ভাঙতে পারে, তখন Java-র sort কখনো কখনো “Comparison method violates its general contract!” বলে exception দেয়।)

  3. বই খুঁজে না পাওয়ার রহস্য: একটি লাইব্রেরি অ্যাপে Book(isbn, title, copiesAvailable)-এর equals আর hashCode তিনটি field দিয়েই লেখা (IDE-তে সব field-এ টিক)। বইগুলো একটি HashSet<Book> catalog-এ। একজন পাঠক একটি বই ধার নিলেন, কোড book.setCopiesAvailable(2) চালাল। পরদিন catalog.contains(book) false দিচ্ছে। কেন? দুটি আলাদা সমাধান বলুন।

    copiesAvailable বদলাতেই book-এর hashCode বদলেছে, কিন্তু object এখনো পুরোনো hash-এর ঘরে বসে আছে। contains নতুন hash দিয়ে অন্য ঘরে খোঁজে, পায় না — অংশ ৬-এর mutable key ফাঁদ। সমাধান ১ (সঠিক নকশা): equals আর hashCode শুধু isbn দিয়ে লিখুন — বইয়ের পরিচয় ISBN, কপির সংখ্যা তার অবস্থা। সমাধান ২: অবস্থাকে key থেকে আলাদা করুন — যেমন Map<String, Integer> stock (ISBN → কপি সংখ্যা), আর Book নিজে immutable থাকুক। HashSet থেকে বের করে বদলে আবার ঢোকানো কাজ করে, কিন্তু ভুলে যাওয়া সহজ, তাই এটা সমাধান নয়, জোড়াতালি।

  4. নিজে লিখুন (একমাত্র কোড লেখার কাজ): Money(long poisha, String currency)-কে Comparable<Money> বানান। দুটি ভিন্ন মুদ্রা (BDT আর USD) তুলনা করতে বললে কী করা উচিত? আর এক লাইনে বলুন, একই কাজ record দিয়ে করলে কোন অংশগুলো আর লিখতে হবে না।

    একটি যুক্তিসঙ্গত উত্তর:

    @Override
    public int compareTo(Money other) {
        if (!currency.equals(other.currency))
            throw new IllegalArgumentException("currency mismatch: " + currency + " vs " + other.currency);
        return Long.compare(poisha, other.poisha);
    }

    BDT আর USD-এর তুলনা বিনিময় হার ছাড়া অর্থহীন, তাই নীরবে ভুল উত্তরের চেয়ে exception ভালো (fail-fast). আরেকটি বৈধ নকশা হলো আগে currency-র নাম, তারপর পরিমাণ দিয়ে সাজানো — তাহলে sort কখনো ভাঙে না, আর compareTo শূন্য হয় ঠিক তখনই যখন equals true (consistent with equals); কিন্তু “USD 1 < BDT 500” ধরনের অর্থহীন ক্রম আসে। খেয়াল করুন, Long.compare — poisha long, তাই Integer.compare নয়, আর বিয়োগ তো নয়ই। record দিয়ে লিখলে (record Money(long poisha, String currency) implements Comparable<Money>) constructor, equals, hashCode, toString আর getter লিখতে হয় না — compareTo তবু নিজেকেই লিখতে হয়, কারণ record ক্রম জানে না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমHashSet-এ set.contains(x) ডাকলে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔢 x.hashCode() ডেকে একটি int পাওয়া যায়
  2. 🗂️ সেই int-এর বিট মিশিয়ে টেবিলের আকার দিয়ে ছেঁটে একটি bucket বাছা হয়
  3. 🔍 শুধু সেই bucket-এর উপাদানগুলো দেখা হয়
  4. ⚖️ প্রতিটির সংরক্ষিত hash মান x-এর hash-এর সাথে মেলানো হয়
  5. ✅ hash মিললে equals ডেকে নিশ্চিত হওয়া হয়, তারপর true বা false ফেরত

একটি Java class equals override করেছে (roll আর name মেলায়) কিন্তু hashCode করেনি। একই roll ও name-এর দুটি নতুন object একটি HashSet-এ add করলে সাধারণত কী ঘটে?

  • compile error হয়
  • set-এ একটিই থাকে, কারণ equals true দেয়
  • set-এ দুটিই থাকে, size 2
  • চলার সময় exception আসে

Java-র equals contract-এর অংশ নয় কোনটি?

  • x.equals(x) সবসময় true
  • x.equals(y) true হলে y.equals(x)-ও true
  • x.equals(null) false দেয়
  • সমান দুটি object-এর toString একই হতে হবে

class Book-এ লেখা আছে: @Override public boolean equals(Book other) { … }। কী হবে?

  • ঠিকঠাক compile হয়, Object-এর equals override হয়
  • compile error: method does not override or implement a method from a supertype
  • compiler নিজে থেকে parameter Object করে নেয়
  • compile হয়, কিন্তু চলার সময় ClassCastException

a.equals(b) true হলে Java-র চুক্তি অনুযায়ী কোনটি অবশ্যই সত্য?

  • a.hashCode() == b.hashCode()
  • a == b
  • a.compareTo(b) == 0
  • a.toString().equals(b.toString())

compareTo-তে return this.value - other.value; লেখা ঝুঁকিপূর্ণ কেন?

  • বিয়োগ ধীর, তাই sort ধীর হয়
  • compareTo শুধু -1, 0 বা 1 ফেরত দিতে পারে
  • integer overflow-এ চিহ্ন উল্টে গিয়ে ভুল ক্রম আসতে পারে
  • এতে compile error হয়

toString override না করা একটি Java class-এর object println দিয়ে ছাপলে কী আসে?

  • সব field-এর নাম আর মান
  • class-এর নাম, @ আর একটি hex সংখ্যা
  • object-এর আসল মেমরি ঠিকানা দশমিকে
  • একটি খালি লাইন

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Object-এর method-গুলো আপনার class আর Java library-র চুক্তি: println ডাকে toString, collection ডাকে equals ও hashCode, sort ডাকে compareTo. নিয়ম ভাঙলে error নয়, নীরব ভুল উত্তর।
  2. toString প্রায় সব class-এ override করুন — গুরুত্বপূর্ণ field দেখান, গোপন তথ্য কখনো নয়, আর এর লেখা parse করবেন না।
  3. equals: equals(Object o), @Override, this == o → instanceof pattern → শুধু পরিচয়ের field (null হলে Objects.equals). পাঁচ শর্ত: reflexive, symmetric, transitive, consistent, non-null. Value class final বা record রাখুন।
  4. equals override = hashCode override, একই field দিয়ে, Objects.hash. না হলে HashSet-এ ডুপ্লিকেট আর ব্যর্থ contains. HashSet/HashMap-এ রাখা object-এর পরিচয়-field কখনো বদলাবেন না।
  5. compareTo: Integer.compare/Long.compare, কখনো বিয়োগ নয়; সম্ভব হলে equals-এর সাথে সামঞ্জস্যপূর্ণ। এক class-এর একটিই natural ordering — বাকি ক্রমের জন্য Comparator (পর্ব ৩৩)।

পরের পর্বে: সপ্তাহের সাতটি দিন, অর্ডারের চারটি অবস্থা, ট্রাফিক লাইটের তিনটি রং — যখন সম্ভাব্য মান মাত্র কয়েকটি আর নির্দিষ্ট, তখন String বা int কেন বিপজ্জনক? Java-র enum: নিজস্ব field আর method-ওয়ালা, নিরাপদ, আর == দিয়ে তুলনা করা যায় এমন টাইপ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান