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

Java polymorphism আর dynamic dispatch — এক ডাক, অনেক আচরণ

Java Polymorphism — Static vs Runtime Type, Upcasting, Dynamic Dispatch, Downcasting and instanceof Patterns

Taught in Bangla, this lesson explains polymorphism through a payment checkout: static versus runtime type, upcasting, how dynamic dispatch uses vtables, which methods are not virtual, overloading versus overriding, downcasting with instanceof pattern matching, what isn't polymorphic, and replacing instanceof chains with polymorphism under the Open/Closed principle.

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

  • Static type আর runtime type-এর পার্থক্য, upcasting, আর compiler কী অনুমতি দেয় বনাম আসলে কী চলে
  • Dynamic dispatch ভেতরে কীভাবে কাজ করে — vtable, আর Java-তে কোন method virtual নয়
  • Compile-time polymorphism (overloading) বনাম runtime polymorphism (overriding), আর দুটো মিলে যে ফাঁদ
  • Downcasting, instanceof, Java 16-এর pattern matching আর ClassCastException
  • Field hiding, static method, constructor থেকে override-যোগ্য method ডাকার বিপদ, আর instanceof-শিকলের বদলে polymorphism (Open/Closed নীতি)

১ · একটি checkout, অনেক রকম পেমেন্ট

ধরুন আপনি একটি অনলাইন দোকানের checkout লিখছেন। খদ্দের bKash, Nagad বা কার্ড — যেকোনো উপায়ে টাকা দিতে পারেন, আর প্রতিটির চার্জ আলাদা নিয়মে হিসাব হয়। সবচেয়ে সরল চিন্তা হলো একটি বড় if-else: “যদি bKash হয় তাহলে এই হিসাব, যদি Nagad হয় তাহলে ওই হিসাব …”. প্রথম দিন এটা চলে। কিন্তু ছয় মাস পরে Rocket যোগ হলো, তারপর Upay, তারপর ব্যাংক ট্রান্সফার। প্রতিবার ফিরে গিয়ে সেই পুরোনো, পরীক্ষিত checkout কোড খুলতে হয় — আর প্রতিবার কিছু একটা ভাঙার ঝুঁকি।

পর্ব ১৯-এ দেখেছেন, একটি class আরেকটিকে extends করে তার আচরণ পায়, আর @Override দিয়ে কোনো method নতুন করে লিখতে পারে। এই পর্বে দেখবেন সেই ক্ষমতার আসল ফল: checkout শুধু জানবে “এটি একটি Payment”, আর ডাকবে payment.fee(amount). কোন হিসাব চলবে, তা ঠিক করবে আসল object নিজে। এই ক্ষমতার নাম polymorphism — গ্রিক শব্দ, মানে “বহু রূপ”। একটি ডাক, অনেক রকম আচরণ।

মোড়ের চায়ের দোকানে মালিক তিনজন কর্মচারীকে একই কথা বলেন: “কাজ শুরু করুন।” একজন চা বানাতে বসেন, একজন কাপ ধোন, একজন ক্যাশে বসেন। মালিক প্রত্যেককে আলাদা করে বুঝিয়ে দেন না কী করতে হবে — প্রত্যেকে নিজের দায়িত্ব জানেন। নতুন একজন রুটি-বানানো কর্মচারী এলে মালিকের কথা বদলায় না; নতুন মানুষটি “কাজ শুরু করুন” শুনে নিজের কাজ শুরু করেন। মালিক হলেন checkout কোড, “কাজ শুরু করুন” হলো fee(), আর কর্মচারীরা হলেন BkashPayment, NagadPayment, CardPayment.

কোডে এই ধারণা দেখতে এরকম (পুরো, চালানো যায় এমন সংস্করণ অংশ ৪-এ):

class Payment {
    double fee(double amount) { return 0; }
}
class BkashPayment extends Payment {
    @Override double fee(double amount) { return amount * 0.0185; }
}

Payment p = new BkashPayment();   // ভ্যারিয়েবল Payment, object BkashPayment
double charge = p.fee(1000);      // চলে BkashPayment-এর fee() → 18.5

শেষ লাইনটিই পুরো পর্বের প্রশ্ন। ভ্যারিয়েবল p-এর টাইপ Payment, যার fee() ফেরত দেয় 0. অথচ চলে BkashPayment-এর fee(), ফল 18.5. কোন method চলবে, তা ঠিক হলো ভ্যারিয়েবলের লেখা টাইপ দেখে নয়, চলার সময় আসল object দেখে। এই চলার-সময়ের বাছাইয়ের নাম dynamic dispatch. এখানে যে চার্জের হারগুলো ব্যবহার করছি, সেগুলো শুধু উদাহরণের জন্য ধরা — আসল হার প্রতিষ্ঠান ও সময়ভেদে আলাদা।

মূল ধারণা

Polymorphism-এর আসল লাভ: যে কোড টাইপগুলো ব্যবহার করে, তাকে টাইপগুলোর নাম জানতে হয় না। Checkout জানে শুধু Payment. কাল RocketPayment এলে আপনি একটি নতুন class লিখবেন; checkout-এর একটি লাইনও বদলাবে না। এভাবেই বড় Java সিস্টেম নতুন টাইপের জন্য খোলা থাকে, অথচ পুরোনো কোড ছুঁতে হয় না।

“Polymorphism” শব্দটি প্রোগ্রামিং ভাষায় আনেন ব্রিটিশ কম্পিউটার বিজ্ঞানী Christopher Strachey, ১৯৬৭ সালের একটি বিখ্যাত লেকচার-নোটে। একই বছর নরওয়েতে Ole-Johan Dahl ও Kristen Nygaard-এর Simula 67 ভাষায় আসে class, subclass আর “virtual” procedure — আজকের dynamic dispatch-এর পূর্বপুরুষ। সেখান থেকে ধারণাটি যায় Smalltalk আর C++-এ, আর ১৯৯৫ সালে Java একে করে ভাষার স্বাভাবিক আচরণ।

২ · Static type আর runtime type — ভ্যারিয়েবলের দুই পরিচয়

Polymorphism বোঝার চাবি একটি পার্থক্য। প্রতিটি reference ভ্যারিয়েবলের দুটি টাইপ আছে:

Static type

Static type হলো কোডে যা লেখা: Payment p-এ Payment. এটি কখনো বদলায় না, আর compiler শুধু এটাই দেখে।

compile-এর সময়

Runtime type

Runtime type হলো ভ্যারিয়েবল এই মুহূর্তে যে object-কে দেখাচ্ছে তার class: new BkashPayment() হলে BkashPayment. নতুন object বসালে বদলে যায়।

চলার সময়

Payment p = new BkashPayment(); — এখানে ডান দিকে একটি subclass-এর object, বাঁ দিকে superclass-এর ভ্যারিয়েবল। কোনো cast লিখতে হয়নি, কারণ এটি সবসময় নিরাপদ: প্রতিটি BkashPayment একটি Payment (পর্ব ১৯-এর is-a সম্পর্ক)। Subclass-এর object-কে superclass-এর টাইপে দেখা — একে বলে upcasting. Java এটা নিজে থেকেই করে: method-এর parameter Payment হলে আপনি BkashPayment পাঠাতে পারেন, Payment[] array-তে NagadPayment রাখতে পারেন।

ঢাকা মেট্রোর গেটে কার্ড ছোঁয়ালে গেট শুধু দেখে “এটি একটি বৈধ মেট্রো কার্ড কি না”। কার্ডটি ছাত্রের, অফিসযাত্রীর না একবারের টিকিট — সেই খুঁটিনাটি গেটের মাথাব্যথা নয়। কিন্তু ভাড়া কাটার সময় আসল কার্ডের নিয়মই খাটে। গেটের “বৈধ কার্ড” দেখা হলো static type; ভাড়া কাটার নিয়ম আসে runtime type থেকে।

Compiler কী অনুমতি দেয়, আর আসলে কী চলে

এই দুই টাইপ দুটি আলাদা প্রশ্নের উত্তর দেয়, আর এটাই পর্বের সবচেয়ে জরুরি নিয়ম:

  • “এই method ডাকা কি বৈধ?” — উত্তর দেয় static type, compile-এর সময়। Compiler শুধু Payment class-এ খোঁজে।
  • “কোন সংস্করণ চলবে?” — উত্তর দেয় runtime type, চলার সময়। JVM আসল object-এর class-এ খোঁজে।

ধরুন SavingsAccount-এ একটি নতুন method আছে interest(), যা Account-এ নেই। তখন:

Account a = new SavingsAccount();
System.out.println(a.interest());   // object-এ interest() আছে, তবুও…
Main.java:10: error: cannot find symbol
        System.out.println(a.interest());
                            ^
  symbol:   method interest()
  location: variable a of type Account
1 error

Compiler-এর বার্তার শেষ লাইনটি পড়ুন: “variable a of type Account”. Object-টি আসলে SavingsAccount কি না, compiler তা জানে না এবং জানার চেষ্টাও করে না — কারণ a-তে পরে যেকোনো Account বসতে পারে, হয়তো একটি CurrentAccount, যার interest() নেই। Compiler তাই শুধু সেই প্রতিশ্রুতিটুকু মানে যা টাইপে লেখা। Upcasting-এর দাম এটাই: আপনি নমনীয়তা পান, কিন্তু subclass-এর বিশেষ method আর সরাসরি দেখা যায় না। সেগুলো ফেরত পাওয়ার উপায় (downcasting) অংশ ৬-এ।

মূল ধারণা

Static type ঠিক করে কী ডাকা যাবে; runtime type ঠিক করে কোনটা চলবে। এই এক বাক্যে পর্বের প্রায় সব প্রশ্নের উত্তর আছে — ফাঁদের প্রশ্নগুলোরও।

৩ · Dynamic dispatch — JVM কীভাবে সঠিক method খুঁজে পায়

p.fee(1000) লাইনটি দুই জায়গায় দুইবার “বিচার” হয়। প্রথমবার javac, compile-এর সময়: সে Payment-এ fee(double) খুঁজে পায়, আর bytecode-এ একটি নির্দেশ লেখে — মোটামুটি “invokevirtual Payment.fee(double)”, অর্থাৎ “এই signature-এর virtual method ডাকো”। কোন class-এর সংস্করণ, তা সে লেখে না, কারণ জানে না। দ্বিতীয়বার JVM, চলার সময়: সে p-এর object-এর কাছে যায়, দেখে object-টি কোন class-এর, আর সেই class-এর virtual method table (vtable) থেকে আসল কোড খুঁজে নেয়।

প্রতিটি object-এর ভেতরে (তার header-এ) লুকানো থাকে তার class-এর একটি সূত্র। আর প্রতিটি class-এর আছে একটি টেবিল — প্রতিটি override-যোগ্য method-এর জন্য একটি ঘর, ঘরে লেখা “এই method ডাকলে এই কোড চালাও”। Subclass parent-এর টেবিলের কপি দিয়ে শুরু করে; যে method override করে, শুধু সেই ঘরে নিজের কোডের ঠিকানা বসায়। নিচের ডায়াগ্রামে “পরে” চাপুন:

  1. কোডp.fee(1000)Payment p
  2. javacPayment-এ খোঁজstatic type
  3. ObjectBkashPaymentruntime type
  4. vtablefee ঘরআসল কোডের ঠিকানা
  5. ফল18.5BkashPayment.fee
Main.javaআপনি যা লিখেছেন
Payment p = new BkashPayment();   // upcasting, নিজে থেকেই
double charge = p.fee(1000);
javac-এর চিন্তাশুধু static type দেখে
p-এর টাইপ?            Payment
Payment-এ fee(double)? আছে → ডাক বৈধ
bytecode-এ লেখো:       invokevirtual Payment.fee:(D)D
// কোন class-এর fee চলবে — javac জানে না, লেখেও না
heapp যে object দেখায়
p ──► ┌──────────────────────┐
      │ header: class = BkashPayment │
      │ (field থাকলে এখানে)   │
      └──────────────────────┘
BkashPayment-এর vtableসরলীকৃত ছবি
toString()    → Object.toString        (উত্তরাধিকার)
equals(Object)→ Object.equals          (উত্তরাধিকার)
hashCode()    → Object.hashCode        (উত্তরাধিকার)
fee(double)   → BkashPayment.fee       (override)
চললBkashPayment.fee(1000)
return amount * 0.0185;   // 1000 × 0.0185
charge = 18.5
  1. আপনার কোড। একটি BkashPayment object তৈরি হলো, আর তার reference রাখা হলো Payment টাইপের ভ্যারিয়েবলে। এটি upcasting — কোনো cast লিখতে হয়নি।
  2. javac শুধু static type দেখে। Payment-এ fee(double) আছে, তাই ডাক বৈধ। Bytecode-এ সে লেখে একটি invokevirtual নির্দেশ, যাতে শুধু method-এর নাম আর signature থাকে। কোন class-এর সংস্করণ চলবে, তা সিদ্ধান্ত ছাড়াই রেখে দেয়।
  3. চলার সময় JVM object-এর কাছে যায়। p ধরে heap-এ object-টি পায়, আর তার header থেকে পড়ে: এর class হলো BkashPayment. ভ্যারিয়েবলের টাইপ Payment — সেটা এখন আর কোনো ভূমিকা রাখে না।
  4. vtable-এর ঘর। BkashPayment-এর টেবিলে fee(double)-এর ঘরে লেখা আছে BkashPayment.fee, কারণ এই class সেটি override করেছে। যে method override করেনি (যেমন toString), সেগুলোর ঘরে parent-এর কোডই থেকে যায়। এই ছবিটি সরলীকৃত — আসল JVM-এ টেবিলের বিন্যাস আরও জটিল, কিন্তু ধারণা একই।
  5. সঠিক কোড চলল। 1000 × 0.0185 = 18.5. পুরো খোঁজাখুঁজি একটি টেবিলের ঘর পড়ার মতো সস্তা, আর JIT সাধারণত এটাকে আরও দ্রুত করে (ভাবনার প্রশ্ন ৩)।

Java-তে কোন method virtual — আর কোনটা নয়

Java-র নিয়ম সরল: প্রতিটি instance method নিজে থেকেই virtual, শুধু এই ক্ষেত্রগুলো ছাড়া:

method-এর ধরনoverride করা যায়?কোনটা চলবে, কে ঠিক করে
static methodনা — একই signature লিখলে হয় method hidingcompile-এর সময়, static type দেখে
private methodনা — subclass দেখতেই পায় নাcompile-এর সময়
final methodনা — চেষ্টা করলে compile errorএকটিই সংস্করণ আছে
constructorনা — constructor উত্তরাধিকারে আসে নাnew-এর পরে লেখা class
বাকি সব instance methodহ্যাঁচলার সময়, runtime type দেখে
C/C++/Python জানলে

C++-এ ঠিক উল্টো: method নিজে থেকে non-virtual, dynamic dispatch চাইলে virtual লিখতে হয়, আর polymorphism কাজ করে শুধু pointer বা reference দিয়ে — object কপি করলে subclass-এর অংশ কেটে যায় (slicing). Java-তে object ভ্যারিয়েবল সবসময় reference, তাই slicing হয় না, আর virtual শব্দই নেই। Python-এ সব method-ই কার্যত virtual, কিন্তু টাইপ-যাচাই চলার সময় হয় (duck typing), compile-এর সময় নয়। C-তে একই আচরণ পেতে হাতে function pointer-এর টেবিল বানাতে হয়। তুলনার জন্য দেখুন C++-এর polymorphism ও virtual আর Python-এর inheritance পর্ব।

৪ · Polymorphic array — এক লুপ, অনেক আচরণ

Polymorphism-এর শক্তি সবচেয়ে স্পষ্ট দেখা যায় যখন অনেক রকম object একটি তালিকায় থাকে। Payment[] array-তে bKash, Nagad, Card — তিন রকম object রাখা যায়, কারণ প্রতিটিই একটি Payment. তারপর একটি লুপ সবগুলোর ওপর fee() ডাকে। চালিয়ে দেখুন:

class Payment {
    String name() { return "Payment"; }
    double fee(double amount) { return 0; }
}
class BkashPayment extends Payment {
    @Override String name() { return "bKash"; }
    @Override double fee(double amount) { return amount * 0.0185; }
}
class NagadPayment extends Payment {
    @Override String name() { return "Nagad"; }
    @Override double fee(double amount) { return amount * 0.0125; }
}
class CardPayment extends Payment {
    @Override String name() { return "Card"; }
    @Override double fee(double amount) { return 10 + amount * 0.01; }
}
class Main {
    public static void main(String[] args) {
        Payment[] payments = { new BkashPayment(), new NagadPayment(), new CardPayment() };
        double total = 0;
        for (Payment p : payments) {
            double f = p.fee(1000);              // কোন fee()? object ঠিক করবে
            System.out.printf("%-6s fee: %6.2f%n", p.name(), f);
            total += f;
        }
        System.out.printf("Mot fee: %.2f%n", total);
    }
}

আউটপুট চার লাইন: bKash fee: 18.50, Nagad fee: 12.50, Card fee: 20.00, Mot fee: 51.00. লুপের ভেতরের লাইন p.fee(1000) একটিই, কিন্তু তিনবার তিনটি আলাদা method চলেছে — কারণ p-এর static type সবসময় Payment হলেও runtime type প্রতিবার আলাদা। Card-এর হিসাবে নির্দিষ্ট ১০ টাকা আর ১% যোগ হয়েছে, যা অন্য দুটির থেকে ভিন্ন সূত্র; লুপকে সেটা জানতেই হয়নি। প্রতিটি override-এ @Override লেখা আছে — বানান ভুল করলে (যেমন Fee) compiler সাথে সাথে ধরবে (পর্ব ১৯)। এখানে সব class একই ফাইলে আর public ছাড়া লেখা, যাতে অনলাইনে চলে; নিজের প্রজেক্টে প্রতিটি সাধারণত নিজের .java ফাইলে public class হিসেবে থাকে।

লুপের প্রতিটি ঘূর্ণনে কী ঘটছে, ধাপে ধাপে দেখুন:

1Payment[] payments = { new BkashPayment(),2    new NagadPayment(), new CardPayment() };3double total = 0;4for (Payment p : payments) {5    double f = p.fee(1000);6    System.out.printf(..., p.name(), f);7    total += f;8}9System.out.printf("Mot fee: %.2f%n", total);
প্রতিটি ধাপে
p-এর static type
p-এর runtime type
যে fee() চলল
f
total
  1. Array তৈরি। তিনটি ভিন্ন class-এর object একটি Payment[]-এ বসল — প্রতিটি upcast হয়েছে নিজে থেকেই। Array-র প্রতিটি ঘরে আছে একটি reference; object-গুলো নিজের নিজের class মনে রেখেছে।
  2. প্রথম ঘূর্ণন। p দেখাচ্ছে BkashPayment-কে। JVM তার vtable থেকে BkashPayment.fee চালায়: 1000 × 0.0185 = 18.50. p.name()-ও একইভাবে dispatch হয়ে দেয় “bKash”.
  3. দ্বিতীয় ঘূর্ণন — একই লাইন, অন্য কোড। এবার p দেখাচ্ছে NagadPayment-কে, তাই চলে NagadPayment.fee: 12.50. লাইন ৫ একটুও বদলায়নি; বদলেছে শুধু object.
  4. তৃতীয় ঘূর্ণন। CardPayment.fee: 10 + 1000 × 0.01 = 20.00. সম্পূর্ণ ভিন্ন সূত্র, অথচ লুপের কোনো if লাগেনি।
  5. লুপ শেষ। মোট 51.00. এবার ভাবুন: চতুর্থ একটি RocketPayment যোগ করতে কী বদলাতে হবে? শুধু নতুন class লিখে array-তে একটি object যোগ — লুপ যেমন ছিল তেমনই থাকবে (অনুশীলন ১)।

একই নকশা প্রতিদিনের অনেক সমস্যায় খাটে:

Shape-এর ক্ষেত্রফল

Shape-এর area(); Circle দেয় πr², Rectangle দেয় দৈর্ঘ্য × প্রস্থ। একটি জমির নকশায় সব টুকরোর মোট ক্ষেত্রফল — এক লুপে।

গণিত

Notification পাঠানো

Notifier-এর send(msg); SmsNotifier, EmailNotifier, PushNotifier নিজের পথে পাঠায়। OTP পাঠানোর কোড জানে না কোন পথে গেল।

ব্যাকএন্ড

Java-র নিজের library

List<String> list = new ArrayList<>(); — static type List, runtime type ArrayList. পর্ব ৩০-এ এটাই প্রতিদিনের অভ্যাস হবে।

সবখানে

৫ · Overloading বনাম overriding — compile-time আর runtime polymorphism

পর্ব ১২-এ দেখেছিলেন overloading — একই নাম, ভিন্ন parameter-তালিকা। সেটাও এক ধরনের “বহু রূপ”: print(int) আর print(String) একই নামে ভিন্ন কাজ করে। তাই বইয়ে প্রায়ই দুটি নাম দেখবেন:

Compile-time polymorphismRuntime polymorphism
Java-তে কীভাবেOverloadingOverriding (dynamic dispatch)
কোথায়একই class-এ (বা উত্তরাধিকারে পাওয়া) একই নাম, ভিন্ন parametersubclass-এ হুবহু একই signature
কে বাছেcompilerJVM
কী দেখে বাছেargument-গুলোর static typereceiver object-এর runtime type
কখনcompile-এর সময়, একবারইপ্রতিটি ডাকের সময়

শেষ দুই সারিই ফাঁদের উৎস। Overload বাছাইয়ে compiler শুধু argument-এর লেখা টাইপ দেখে, ভেতরে আসলে কী আছে তা নয়। একটি ছোট উদাহরণ:

static String check(Object o) { return "Object overload"; }
static String check(String s) { return "String overload"; }

Object o = "Dhaka";            // ভেতরে আসলে একটি String
System.out.println(check(o));  // ছাপে: Object overload

o-এর ভেতরে একটি String, তবুও চলে check(Object). কারণ compiler বাছাইয়ের সময় দেখেছে o-এর static type Object, আর Object-কে check(String)-এ পাঠানো যায় না। চলার সময় এই সিদ্ধান্ত আর বদলায় না — overload-এর জন্য কোনো dynamic dispatch নেই। (অংশ ৭-এর প্রোগ্রামে এটি চালিয়ে দেখবেন।)

এবার দুটো একসাথে। একটি Notifier আর তার subclass SmsNotifier, দুটিতেই send-এর দুটি overload. কোন লাইন কী ছাপবে, প্রতিটি ধাপে আগে নিজে অনুমান করুন:

1class Notifier {2    void send(Object msg) { print("Notifier: Object"); }3    void send(String msg) { print("Notifier: String"); }4}5class SmsNotifier extends Notifier {6    @Override void send(Object msg) { print("Sms: Object"); }7    @Override void send(String msg) { print("Sms: String"); }8}9Notifier n = new SmsNotifier();10Object m = "OTP 4821";11n.send(m);12n.send((String) m);
দুই বিচারক, দুই প্রশ্ন
n: static / runtime
argument-এর static type
compiler বাছল (signature)
JVM বাছল (class)
  1. দুটি ভ্যারিয়েবল, চারটি টাইপ। n-এর static type Notifier, runtime type SmsNotifier. m-এর static type Object, runtime type String. প্রশ্ন: লাইন ১১-তে কোন দুটি টাইপ কাজে লাগবে?
  2. Compiler-এর পালা — overload বাছাই। সে n-এর static type Notifier-এ send খোঁজে, আর argument m-এর static type দেখে: Object. send(String) খাটে না (সব Object তো String নয়), তাই সে চিরদিনের জন্য ঠিক করে দেয়: signature হবে send(Object). m-এর ভেতরে যে আসলে একটি String — তা এখানে কোনো ভূমিকা রাখে না।
  3. JVM-এর পালা — override বাছাই। চলার সময় JVM দেখে n-এর object SmsNotifier; তার vtable-এ send(Object)-এর ঘরে আছে SmsNotifier-এর সংস্করণ। ছাপা হয় Sms: Object. অর্থাৎ receiver-এর runtime type কাজে লাগল, কিন্তু argument-এর runtime type লাগল না। একে বলে single dispatch — Java শুধু একটি জিনিসের (receiver) runtime type দেখে।
  4. Cast দিয়ে static type বদলানো। (String) m লিখলে argument-এর static type হয় String, তাই compiler বাছে send(String); JVM আবার SmsNotifier-এর সংস্করণ চালায়: Sms: String. সরাসরি n.send("OTP 4821") লিখলেও একই ফল। পুরো উদাহরণটি OpenJDK 21-এ চালিয়ে যাচাই করা।

মনে রাখার কৌশল: ডটের বাঁ দিকের object (receiver) চলার সময় বিচার হয়; বন্ধনীর ভেতরের argument-গুলো compile-এর সময়। পরীক্ষায় “আউটপুট কী হবে” ধরনের প্রশ্নে প্রথমে compiler হয়ে signature ঠিক করুন, তারপর JVM হয়ে class ঠিক করুন — দুই ধাপ আলাদা রাখলে ভুল হয় না। আর একটি কথা: subclass-এ একই নামের method লিখে parameter টাইপ সামান্য বদলে ফেললে (send(String)-এর জায়গায় send(CharSequence)) সেটা override নয়, নতুন overload — @Override লিখে রাখলে compiler এই ভুল ধরে দেয়।

৬ · Downcasting, instanceof আর pattern matching

কখনো কখনো upcast করা object-এর বিশেষ ক্ষমতা আবার দরকার হয় — যেমন Account a থেকে SavingsAccount-এর interest(). তখন উল্টো দিকে cast করতে হয়: (SavingsAccount) a. একে বলে downcasting. Upcasting সবসময় নিরাপদ বলে নিজে থেকে হয়; downcasting নিরাপদ নয়, তাই আপনাকে লিখে দায় নিতে হয়। আর ভুল হলে Java চুপ থাকে না:

class Payment {
    double fee(double amount) { return 0; }
}
class BkashPayment extends Payment {
    @Override double fee(double amount) { return amount * 0.0185; }
}
class NagadPayment extends Payment {
    @Override double fee(double amount) { return amount * 0.0125; }
}
class Main {
    public static void main(String[] args) {
        Payment p = new NagadPayment();
        BkashPayment b = (BkashPayment) p;   // compile হয়, কিন্তু…
        System.out.println(b.fee(1000));
    }
}
Exception in thread "main" java.lang.ClassCastException: class NagadPayment cannot be cast to class BkashPayment (NagadPayment and BkashPayment are in unnamed module of loader 'app')
	at Main.main(Main.java:13)

Trace-টি পড়ুন: ClassCastException, “class NagadPayment cannot be cast to class BkashPayment”, আর ঘটেছে লাইন ১৩-তে — ঠিক cast-এর লাইনে। বন্ধনীর ভেতরের module/loader-এর কথাটুকু এখন উপেক্ষা করতে পারেন। (OpenJDK 21-এ যাচাই করা; অনলাইন রানারে ফাইলের নাম prog.java দেখাতে পারে।) Compiler কেন আটকাল না? কারণ Payment থেকে BkashPayment-এ cast সম্ভব — p-তে কোনো দিন একটি BkashPayment থাকতেই পারে। Compiler শুধু অসম্ভব cast আটকায়, যেমন সম্পর্কহীন (String) p. যাচাই হয় চলার সময়, আর মিল না পেলে JVM exception ছোড়ে — C-র মতো নীরবে ভুল মেমরি পড়ে না।

আগে দেখুন, তারপর cast — instanceof

instanceof operator জিজ্ঞেস করে “এই object কি এই টাইপের (বা তার কোনো subclass-এর)?” আর true/false দেয়। null হলে উত্তর সবসময় false, তাই এটি null-নিরাপদ। পুরোনো Java-তে লেখা হতো দুই ধাপে:

// Java 16-এর আগের ধরন: পরীক্ষা, তারপর আবার cast
if (a instanceof SavingsAccount) {
    SavingsAccount s = (SavingsAccount) a;
    System.out.println(s.interest());
}

// Java 16 থেকে: pattern matching for instanceof
if (a instanceof SavingsAccount s) {
    System.out.println(s.interest());   // s এখানে SavingsAccount
}

দ্বিতীয় রূপে instanceof-এর পরে টাইপ আর একটি নাম লিখলে, পরীক্ষা সফল হলে সেই নামে একটি নতুন ভ্যারিয়েবল তৈরি হয়, ইতিমধ্যে cast করা অবস্থায়। একই টাইপ দুবার লেখার ঝামেলা নেই, আর পরীক্ষা এক টাইপের, cast আরেক টাইপের — এমন কপি-পেস্ট ভুলের সুযোগও নেই। একে বলে pattern matching for instanceof; Java 14 ও 15-এ preview ছিল, Java 16-এ স্থায়ী হয়েছে। s-এর scope বুদ্ধিমান: if (!(a instanceof SavingsAccount s)) return; লিখলে তার পরের সব লাইনে s ব্যবহার করা যায়।

কিন্তু আগে নিজেকে জিজ্ঞেস করুন: downcast কেন লাগছে? প্রায়ই উত্তর হলো — যে কাজটি subclass-ভেদে আলাদা, সেটি একটি override-যোগ্য method হওয়া উচিত ছিল। if (a instanceof SavingsAccount s) … else if (a instanceof CurrentAccount c) … দেখলেই সন্দেহ করুন; অংশ ৮-এ এর সমাধান। ব্যতিক্রম আছে — যেমন equals(Object)-এর ভেতরে (পর্ব ২৩), বা বাইরের library থেকে আসা টাইপ যেখানে আপনি method যোগ করতে পারেন না।

৭ · যা polymorphic নয় — field, static method আর constructor-এর ফাঁদ

Dynamic dispatch শুধু instance method-এর জন্য। Field আর static method compile-এর সময়েই static type দেখে ঠিক হয়ে যায়। Subclass-এ parent-এর মতো একই নামের field ঘোষণা করলে সেটা override হয় না — নতুন একটি আলাদা field তৈরি হয়, যা parent-এরটাকে শুধু আড়াল করে। একে বলে field hiding. নিচের প্রোগ্রামে এই পর্বের তিনটি নিয়ম একসাথে, আর শেষে pattern matching:

class Account {
    String type = "Account";
    String describe() { return "general account"; }
    static String bank() { return "Bank A"; }
}
class SavingsAccount extends Account {
    String type = "Savings";                 // field hiding, override নয়
    @Override String describe() { return "savings account"; }
    static String bank() { return "Bank S"; } // method hiding
    double interest() { return 0.06; }
}
class Main {
    static String check(Object o) { return "Object overload"; }
    static String check(String s) { return "String overload"; }
    public static void main(String[] args) {
        Account a = new SavingsAccount();
        System.out.println(a.describe());     // runtime type দেখে
        System.out.println(a.type);           // static type দেখে
        System.out.println(a.bank());         // static type দেখে
        Object o = "Dhaka";
        System.out.println(check(o));         // static type Object
        if (a instanceof SavingsAccount s) {  // Java 16+
            System.out.println("interest: " + s.interest());
        }
    }
}

আউটপুট: savings account, Account, Bank A, Object overload, interest: 0.06. প্রথম লাইন dynamic dispatch — object SavingsAccount, তাই তার describe(). দ্বিতীয় লাইনে চমক: object-এর ভেতরে আসলে দুটি type field আছে (“Account” আর “Savings”), আর a-এর static type Account বলে পড়া হলো parent-এরটা। তৃতীয় লাইনে static method — object-এর সাথে তার সম্পর্কই নেই, তাই Account.bank() চলল। (Object দিয়ে static method ডাকা বৈধ, কিন্তু বিভ্রান্তিকর; সবসময় Account.bank() লিখুন। এখানে শুধু দেখানোর জন্য লেখা।) চতুর্থ লাইন অংশ ৫-এর overload ফাঁদ। শেষে instanceof pattern সফল হয়ে s দিয়ে interest() ডাকা গেছে, যা a দিয়ে যেত না।

শিক্ষা: subclass-এ parent-এর field-এর নাম আবার ব্যবহার করবেন না। Field রাখুন private (পর্ব ১৭), আর বাইরে থেকে পড়তে দিন method দিয়ে — method হলে override করা যায়, তাই polymorphism ঠিকঠাক কাজ করে।

Constructor থেকে override-যোগ্য method ডাকবেন না

এই ফাঁদটি কম পরিচিত কিন্তু বিপজ্জনক। Dynamic dispatch constructor চলার সময়ও কাজ করে — এমনকি তখনো, যখন subclass-এর অংশ এখনো তৈরিই হয়নি:

class Base {
    Base() { init(); }               // parent-এর constructor
    void init() { }
}
class Child extends Base {
    private String name = "Rahim";
    @Override void init() { System.out.println("name = " + name); }
}

new Child();   // ছাপে: name = null

new Child()-এ আগে চলে Base-এর constructor (পর্ব ১৯-এর super() ক্রম)। সেখানে init() ডাকায় dynamic dispatch Child-এর সংস্করণ চালায় — কিন্তু তখনো Child-এর field-এ "Rahim" বসানো হয়নি, তাই আসে ডিফল্ট মান null. name.length() ডাকলে হতো NullPointerException. নিয়ম: constructor থেকে শুধু private, final বা static method ডাকুন। Joshua Bloch-এর Effective Java-তেও এই সতর্কতা আছে।

৮ · instanceof-এর শিকল ভেঙে polymorphism — Open/Closed নীতি

এবার অংশ ১-এর সমস্যায় ফিরি। Polymorphism না জানলে checkout দেখতে হয় এরকম:

// আগে: টাইপ দেখে দেখে সিদ্ধান্ত
static double feeOf(Object payment, double amount) {
    if (payment instanceof BkashPayment) return amount * 0.0185;
    else if (payment instanceof NagadPayment) return amount * 0.0125;
    else if (payment instanceof CardPayment) return 10 + amount * 0.01;
    else throw new IllegalArgumentException("অজানা পেমেন্ট");
}

সমস্যাগুলো দেখুন। এক, প্রতিটি পেমেন্টের নিয়ম তার নিজের class-এ না থেকে অন্য জায়গায় ছড়িয়ে আছে। দুই, শুধু feeOf নয় — refundOf, limitOf, logoOf … প্রতিটিতে একই শিকল। তিন, Rocket যোগ করলে প্রতিটি শিকল খুঁজে খুঁজে বদলাতে হবে; একটি ভুলে গেলে কোড compile হবে, কিন্তু চলার সময় “অজানা পেমেন্ট” exception. আর polymorphism দিয়ে:

// পরে: প্রতিটি টাইপ নিজের নিয়ম জানে
static double checkout(Payment payment, double amount) {
    return amount + payment.fee(amount);   // কোনো if নেই
}
// নতুন টাইপ = নতুন class; checkout অপরিবর্তিত
class RocketPayment extends Payment {
    @Override double fee(double amount) { return amount * 0.018; }
}

এই নকশার একটি নাম আছে: Open/Closed Principle — সফটওয়্যার বাড়ানোর জন্য খোলা (নতুন পেমেন্ট যোগ করা যায়), কিন্তু বদলানোর জন্য বন্ধ (সেজন্য পুরোনো checkout খুলতে হয় না)। পুরোনো কোড না ছুঁলে পুরোনো bug-ও ঢোকে না, আর পুরোনো টেস্টগুলো যেমন ছিল তেমনই পাস করে। বাস্তবে Payment সাধারণত একটি interface বা abstract class হয়, যাতে “ডিফল্ট 0 চার্জ”-এর মতো অর্থহীন সংস্করণ লিখতে না হয় — সেটাই পরের পর্ব (পর্ব ২১)।

তাহলে switch-on-type কি সবসময় খারাপ? না। Java 17-এ sealed টাইপ আর Java 21-এ pattern matching-এর switch স্থায়ী হয়েছে, যেখানে compiler নিশ্চিত করে প্রতিটি টাইপ সামলানো হয়েছে — একটি বাদ পড়লে compile error. যখন টাইপের তালিকা স্থির কিন্তু নতুন নতুন কাজ যোগ হয়, তখন সেটা ভালো পছন্দ। এই ভারসাম্যের নাম Expression Problem (ভাবনার প্রশ্ন ৪); বিস্তারিত পর্ব ২৬-এ।

পরীক্ষা ও ইন্টারভিউতে
  • Java-তে polymorphism কয় প্রকার? — সাধারণত দুই: compile-time (method overloading) আর runtime (method overriding, dynamic dispatch).
  • Static method কি override করা যায়? — না; subclass-এ একই signature লিখলে হয় method hiding, কোনটি চলবে তা static type ঠিক করে।
  • private বা final method override করা যায়? — না; private subclass দেখতে পায় না, final override করতে গেলে compile error.
  • Upcasting আর downcasting-এর পার্থক্য? — Upcasting (subclass → superclass) নিজে থেকে হয় ও নিরাপদ; downcasting-এ cast লিখতে হয়, ভুল হলে চলার সময় ClassCastException.
  • Field কি polymorphic? — না; field পড়া হয় reference-এর static type দেখে (field hiding), শুধু instance method dynamic dispatch পায়।

৯ · AI-এর যুগে — polymorphic নকশা চাইতে শিখুন

AI-কে “বিভিন্ন পেমেন্টের চার্জ হিসাব করার কোড দাও” বললে প্রায়ই পাবেন একটি String type parameter আর লম্বা if-else বা switch — অথবা instanceof-এর শিকল, যেখানে প্রতিটি শাখায় cast. কোড চলে, ডেমোতে চমৎকার। কিন্তু অংশ ৮-এর তিনটি সমস্যাই সাথে আসে। আর ধারণাগত প্রশ্নে AI মাঝে মাঝে overloading আর overriding গুলিয়ে ফেলে, বা বলে “static method-ও override হয়”।

AI যা করেকেন সমস্যাআপনি যা করবেন
টাইপ দেখে if (x instanceof …) বা switch (type) শিকলনতুন টাইপে প্রতিটি শিকল বদলাতে হয়, একটি বাদ পড়লে চলার সময় ভুল“প্রতিটি টাইপ নিজের আচরণ override করবে” — শর্ত দিন
instanceof ছাড়াই সরাসরি downcastভুল object এলে ClassCastExceptionDowncast কেন লাগছে জিজ্ঞেস করুন; লাগলে Java 16-এর pattern ব্যবহার করতে বলুন
Subclass-এ parameter টাইপ সামান্য বদলে “override”, @Override ছাড়াআসলে overload — parent-এর method-ই চলতে থাকেপ্রতিটি override-এ @Override চান
Subclass-এ parent-এর মতো একই নামের fieldField hiding — static type অনুযায়ী ভিন্ন মান পড়া হয়Field private, পড়া method দিয়ে
Constructor থেকে override-যোগ্য method ডাকাSubclass-এর field তখনো null/0Constructor-এ শুধু private/final method

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

আমি Java 21 শিখছি; inheritance আর method overriding জানি। কাজ: [যেমন — bKash, Nagad, Card পেমেন্টের চার্জ হিসাব / SMS, Email, Push notification পাঠানো / বিভিন্ন Shape-এর ক্ষেত্রফল] শর্ত: ১. একটি base type রাখো; প্রতিটি ধরনের আচরণ তার নিজের class-এ override হবে, প্রতিটিতে @Override. ২. যে কোড এগুলো ব্যবহার করে, সেখানে কোনো instanceof, type-String বা switch-on-type থাকবে না। ৩. কোনো downcast থাকলে কেন দরকার তা ব্যাখ্যা করো, আর instanceof pattern (Java 16+) ব্যবহার করো। ৪. Subclass-এ parent-এর নামের কোনো field রাখবে না; constructor থেকে override-যোগ্য method ডাকবে না। ৫. শেষে দেখাও: নতুন একটি ধরন (যেমন Rocket) যোগ করতে কোন কোন ফাইল বদলাতে হবে — উত্তর যেন হয় “শুধু নতুন class”. ৬. কোডে overloading আর overriding কোথায় কোথায় আছে, আলাদা করে চিহ্নিত করো।

AI-এর লেখা polymorphic কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Christopher Strachey ১৯৬৭ সালে polymorphism-কে দুই ভাগে ভাগ করেছিলেন। সেই ভাগ কী, আর Java-র কোন বৈশিষ্ট্য কোন ভাগে পড়ে?

১৯৬৭ সালের আগস্টে কোপেনহেগেনে প্রোগ্রামিং বিষয়ে একটি আন্তর্জাতিক সামার স্কুলে Christopher Strachey একগুচ্ছ লেকচার দেন, যার নোট পরে “Fundamental Concepts in Programming Languages” নামে পরিচিত হয়। নোটটি বহু বছর হাতে হাতে ঘুরেছে; আনুষ্ঠানিকভাবে জার্নালে ছাপা হয় ২০০০ সালে। সেখানে তিনি polymorphism-কে দুই ভাগে ভাগ করেন, আর সেই ভাগ আজও প্রায় প্রতিটি টাইপ-তত্ত্বের বইয়ের শুরুতে থাকে।

Ad-hoc polymorphism. Ad-hoc মানে “এই বিশেষ ক্ষেত্রের জন্য”। একই নাম বা চিহ্ন বিভিন্ন টাইপের জন্য আলাদা আলাদা কোড দিয়ে কাজ করে, আর কোন কোড চলবে তা টাইপ দেখে ঠিক হয়। Strachey-র উদাহরণ ছিল + — পূর্ণসংখ্যার যোগ আর দশমিক সংখ্যার যোগ মেশিনের স্তরে সম্পূর্ণ আলাদা কাজ, শুধু চিহ্নটি এক। Java-তে এর সরাসরি রূপ হলো overloading (print(int), print(String)) আর +-এর String-যোগ।

Parametric polymorphism. Parametric ক্ষেত্রে একটিই কোড, যা টাইপের দিকে না তাকিয়েই সব টাইপে একইভাবে কাজ করে। একটি তালিকার দৈর্ঘ্য গোনার কোড তালিকায় সংখ্যা আছে না নাম, তা জানতে চায় না। Java-তে এটি এসেছে ২০০৪ সালে Java 5-এর generics দিয়ে: List<T> (পর্ব ২৮)। ML আর Haskell-এর মতো ভাষায় এটিই polymorphism-এর প্রধান রূপ।

তৃতীয় রূপটি কোথায়? লক্ষ করুন, এই পর্বের মূল বিষয় — subclass-এর object-কে superclass হিসেবে ব্যবহার আর overriding — Strachey-র দুই ভাগে সরাসরি পড়ে না, কারণ ১৯৬৭ সালে Simula সবে জন্মেছে। ১৯৮৫ সালে Luca Cardelli ও Peter Wegner তাঁদের বিখ্যাত প্রবন্ধে ভাগটি বাড়ান: “universal” polymorphism-এর ভেতরে parametric-এর পাশে রাখেন inclusion polymorphism — যা আজ সাধারণত subtype polymorphism নামে পরিচিত; আর ad-hoc-এর ভেতরে রাখেন overloading ও coercion (যেমন int থেকে double-এ স্বয়ংক্রিয় রূপান্তর)।

Java-র মানচিত্র। তাহলে Java-তে তিনটিই আছে: overloading আর widening (ad-hoc), generics (parametric), আর overriding ও dynamic dispatch (subtype). পরীক্ষায় যাকে “compile-time আর runtime polymorphism” বলা হয়, সেটা আসলে এই বড় মানচিত্রের একটি সরল দুই-ভাগ সংস্করণ — একটি “কখন সিদ্ধান্ত হয়” দিয়ে ভাগ করে, অন্যটি “কীভাবে কোড ভাগাভাগি হয়” দিয়ে।

মূল উপলব্ধি: “Polymorphism” একটি জিনিস নয়, একটি পরিবার। Java-তে overloading, generics আর overriding — তিনটিই “এক নাম, অনেক টাইপ”, কিন্তু তিনটি আলাদা প্রক্রিয়ায়, আলাদা সময়ে কাজ করে।

প্র ০২Java-তে method নিজে থেকেই virtual, অথচ C++ আর C#-এ নয়। এই তিন ভাষা কেন তিন রকম সিদ্ধান্ত নিল?

C++: যা ব্যবহার করেন না, তার দাম দেবেন না। Bjarne Stroustrup C++-এর নকশায় একটি নীতি কঠোরভাবে মেনেছেন — zero-overhead principle. Virtual method মানে প্রতিটি object-এ একটি লুকানো pointer (vptr), আর প্রতিটি ডাকে টেবিল দেখে পরোক্ষ লাফ; সবচেয়ে বড় কথা, compiler তখন ডাকটিকে inline করতে পারে না। ১৯৮০-এর দশকের মেশিনে, আর C-র সাথে সমান গতিতে চলার প্রতিশ্রুতিতে, এই খরচ সবার ঘাড়ে চাপানো অগ্রহণযোগ্য ছিল। তাই C++-এ virtual লিখে আপনি জেনেশুনে দাম দেন। Simula 67-ও একই পথে ছিল — সেখানেও virtual শব্দটি লিখতে হতো।

Java: সরলতা আগে, গতি JIT-এর দায়িত্ব। James Gosling-এর দল Smalltalk-এর মতো সব ডাক dynamic রাখার পথ নিল। কারণ, C++-এ “virtual লিখতে ভুলে যাওয়া” একটি পরিচিত bug: subclass override করে, কিন্তু base pointer দিয়ে ডাকলে parent-এর method চলে — কোনো সতর্কবার্তা ছাড়াই। Java চেয়েছিল object-ভিত্তিক কোড “স্বাভাবিকভাবেই ঠিক” আচরণ করুক। গতির প্রশ্নে তাদের বাজি ছিল runtime-এর ওপর: JVM চলার সময় পুরো class-তালিকা দেখতে পায়, তাই যে method কেউ override করেনি, তাকে সরাসরি ডাকে বদলে inline করতে পারে (প্রশ্ন ৩)। যারা নিশ্চিতভাবে override বন্ধ করতে চান, তাদের জন্য final.

C#: versioning-এর শিক্ষা। ২০০০ সালে C# নকশা করার সময় Anders Hejlsberg-এর দল জেনেশুনে Java-র উল্টো দিকে গেল — method নিজে থেকে non-virtual, override করতে override শব্দ বাধ্যতামূলক। Hejlsberg পরে একটি সাক্ষাৎকারে ব্যাখ্যা করেন, মূল কারণ গতি নয়, versioning: একটি library-র লেখক কোনো method virtual করলে আসলে প্রতিশ্রুতি দেন যে ভবিষ্যতে subclass-এর override ভাঙবে না — আর সব method-এ এমন প্রতিশ্রুতি দেওয়া অসম্ভব। এই সমস্যার নাম “fragile base class problem”: base class-এর নিরীহ পরিবর্তন subclass-এর আচরণ ভেঙে দেয়।

পরের প্রজন্ম কী শিখল। Java নিজেও Java 5-এ @Override এনে C#-এর ভালো দিকটি ঐচ্ছিকভাবে নিয়েছে। Joshua Bloch Effective Java-তে লিখেছেন, inheritance-এর জন্য নকশা ও ডকুমেন্ট করুন, নইলে নিষেধ করুন — অর্থাৎ final. আর JVM-এরই আরেক ভাষা Kotlin (২০১৬-তে ১.০) class ও method নিজে থেকে final রেখেছে, override করতে দিতে হলে open লিখতে হয়।

মূল উপলব্ধি: “Virtual by default” প্রশ্নের উত্তর গতি নয়, অগ্রাধিকার। C++ বেছেছে নিয়ন্ত্রণ ও শূন্য খরচ, Java বেছেছে সরলতা আর JIT-এর ওপর আস্থা, C# আর Kotlin বেছেছে library-র দীর্ঘমেয়াদি নিরাপত্তা।

প্র ০৩প্রতিটি method virtual হলে Java ধীর হওয়ার কথা। HotSpot JVM কীভাবে virtual call প্রায় সরাসরি ডাকের মতো দ্রুত করে?

সরল হিসাবে একটি virtual call-এ তিনটি ধাপ: object থেকে class পড়া, class-এর vtable থেকে ঠিকানা পড়া, তারপর সেই ঠিকানায় পরোক্ষ লাফ। আধুনিক CPU-তে এটা নিজে খুব ব্যয়বহুল নয়। আসল ক্ষতি অন্যখানে — পরোক্ষ ডাক inline করা যায় না। আর inlining হলো অন্য প্রায় সব optimization-এর দরজা: method-এর শরীর ডাকার জায়গায় বসলে তবেই compiler অপ্রয়োজনীয় হিসাব বাদ দিতে, loop সাজাতে পারে। তাই HotSpot-এর আসল লক্ষ্য virtual call-কে inline-যোগ্য করা।

Class Hierarchy Analysis. JVM জানে এই মুহূর্তে কোন কোন class লোড হয়েছে। CHA দিয়ে সে দেখে: Payment.fee-কে কি কেউ override করেছে? যদি না করে থাকে, তাহলে প্রতিটি p.fee() ডাকের পেছনে কার্যত একটিই সংস্করণ — JIT সেটাকে সরাসরি ডাক বানিয়ে inline করে দেয়। একে বলে devirtualization. কিন্তু পরে যদি একটি নতুন subclass লোড হয়ে override করে? HotSpot আগে থেকেই এই অনুমানটি “dependency” হিসেবে লিখে রাখে; নতুন class লোড হলে সংশ্লিষ্ট compiled কোড বাতিল করে deoptimization করে, আর পরে নতুন তথ্য দিয়ে আবার compile করে (পর্ব ০৫)।

Inline cache আর profiling. যেখানে সত্যিই একাধিক subclass আছে, সেখানে HotSpot দেখে বাস্তবে কী আসে। প্রোগ্রাম কিছুক্ষণ চলার সময় interpreter প্রতিটি ডাকের জায়গায় কোন class এসেছিল তার হিসাব রাখে। কোনো জায়গায় যদি সবসময় একই class আসে (monomorphic), JIT সেখানে একটি সস্তা পরীক্ষা বসায় — “class কি BkashPayment? হলে inline করা কোড চালাও, না হলে ধীর পথে যাও”। দুটি class এলে (bimorphic) দুটি পরীক্ষা। এর চেয়ে বেশি রকম এলে (megamorphic) সাধারণত vtable-এর পথেই ফেরে। এই “আগের বার কী এসেছিল মনে রাখা” কৌশলের নাম inline cache; এর জন্ম ১৯৮৪ সালে L. Peter Deutsch ও Allan Schiffman-এর Smalltalk-80 বাস্তবায়নে, আর ১৯৯১ সালে Urs Hölzle, Craig Chambers ও David Ungar Self ভাষার জন্য একে “polymorphic inline cache”-এ রূপ দেন।

ইতিহাসের যোগসূত্র। এটা কাকতালীয় নয়। HotSpot-এর শিকড় Self আর Strongtalk প্রকল্পে; সেই দলের ছোট কোম্পানি Animorphic Systems ১৯৯৭ সালে Sun কিনে নেয়, আর তাদের প্রযুক্তি থেকেই HotSpot জন্মায়। Java-র “সব virtual” সিদ্ধান্ত টিকে আছে কারণ dynamic ভাষার জন্য তৈরি এই কৌশলগুলো তাকে গতি ফিরিয়ে দিয়েছে।

মূল উপলব্ধি: Java-তে polymorphism-এর খরচ প্রায়ই শূন্যের কাছে, কারণ JIT চলার সময় বাস্তব তথ্য দেখে অনুমান করে, আর অনুমান ভুল হলে পিছিয়ে আসে। তাই গতির ভয়ে polymorphism এড়াবেন না; পরিমাপ না করে “final দিয়ে দ্রুত করা”-ও সাধারণত অর্থহীন।

প্র ০৪Philip Wadler-এর “Expression Problem” কী? Polymorphism আর pattern matching-এর মধ্যে এটি কোন বিনিময় দেখায়?

১৯৯৮ সালের নভেম্বরে, Java-তে generics আনার আলোচনার একটি mailing list-এ, Philip Wadler একটি পুরোনো সমস্যাকে নাম দেন — Expression Problem. সমস্যাটি আগেও চেনা ছিল (John Reynolds ১৯৭৫ সালে, William Cook ১৯৯০ সালে এ নিয়ে লিখেছেন), কিন্তু Wadler-এর পরিষ্কার সংজ্ঞাই টিকে গেছে। প্রশ্নটা এই: একটি ডেটা-টাইপের পরিবারে নতুন রকম (case) যোগ করা আর নতুন কাজ (operation) যোগ করা — দুটোই কি পুরোনো কোড না বদলে, আলাদাভাবে compile করে, টাইপ-নিরাপদভাবে করা যায়?

একটি ছক কল্পনা করুন। সারিতে টাইপ — Bkash, Nagad, Card; কলামে কাজ — fee, refund, limit. প্রতিটি ঘরে একটি কোডের টুকরো। এখন দুই রকম সংগঠন সম্ভব।

Object-oriented পথ (এই পর্ব): প্রতিটি সারি একটি class, তার ভেতরে সব কাজ। নতুন টাইপ যোগ করা সহজ — একটি নতুন class, পুরোনো কিছু ছুঁতে হয় না (Open/Closed). কিন্তু নতুন কাজ, ধরুন cashback(), যোগ করতে হলে প্রতিটি class খুলে method যোগ করতে হয়।

Functional / pattern-matching পথ: প্রতিটি কলাম একটি function, ভেতরে টাইপ দেখে switch. নতুন কাজ যোগ করা সহজ — একটি নতুন function. কিন্তু নতুন টাইপ এলে প্রতিটি function-এর switch বদলাতে হয়। Haskell বা ML-এর algebraic data type এই পথের স্বাভাবিক রূপ।

Java-তে দুই পথই আছে। Java 17-এ sealed interface আর Java 21-এ pattern matching-এর switch স্থায়ী হওয়ায় দ্বিতীয় পথটিও নিরাপদ হয়েছে: টাইপের তালিকা sealed হলে compiler জানে সব রকম কী কী, আর কোনো switch-এ একটি রকম বাদ পড়লে compile error দেয় — অংশ ৮-এর instanceof-শিকলের “চলার সময় অজানা পেমেন্ট” সমস্যা থাকে না। পর্ব ২৬-এ এটি বিস্তারিত দেখবেন। Object-oriented পথে নতুন কাজ যোগ করার পুরোনো কৌশল হলো Visitor pattern, যা আসলে এই একই ছককে ঘুরিয়ে লেখা।

কখন কোনটা? টাইপ ঘন ঘন যোগ হয় কিন্তু কাজ স্থির — যেমন পেমেন্ট গেটওয়ে, plugin, notification চ্যানেল — সেখানে polymorphism. টাইপ স্থির কিন্তু কাজ বাড়তেই থাকে — যেমন একটি গাণিতিক expression-এর গাছে evaluate, print, simplify, differentiate — সেখানে sealed টাইপ আর pattern matching. দুটোই একসাথে খোলা রাখার সাধারণ সমাধান খুঁজতে গবেষকেরা কয়েক দশক ধরে নতুন ভাষা-কৌশল প্রস্তাব করে চলেছেন।

মূল উপলব্ধি: কোনো নকশা সব দিকে খোলা নয়। Polymorphism টাইপের দিকে খোলা, pattern matching কাজের দিকে খোলা — কোন দিকে আপনার সিস্টেম বেশি বাড়বে, সেটা ভেবেই বেছে নিন।

প্র ০৫Bertrand Meyer-এর Open/Closed Principle (১৯৮৮) আসলে কী বলেছিল, আর আজ আমরা একে কীভাবে বুঝি? এটা কি সবসময় মানা উচিত?

মূল সংজ্ঞা। ১৯৮৮ সালে Bertrand Meyer তাঁর Object-Oriented Software Construction বইয়ে লেখেন, একটি module একই সাথে “open” আর “closed” হওয়া উচিত। Open — অর্থাৎ বাড়ানো যায়: নতুন field বা আচরণ যোগ করা যায়। Closed — অর্থাৎ অন্যরা ব্যবহারের জন্য তৈরি, স্থিতিশীল, আর তার উৎস কোড আর বদলানো হবে না। আপাতবিরোধী এই দুই দাবি মেলানোর উপায় হিসেবে Meyer দেখিয়েছিলেন inheritance: পুরোনো class না ছুঁয়ে তার একটি subclass বানিয়ে নতুন আচরণ যোগ করুন। Meyer-এর নিজের ভাষা Eiffel এই চিন্তার ওপর দাঁড়িয়ে।

পরের ব্যাখ্যা। ১৯৯৬ সালে Robert C. Martin একটি প্রবন্ধে নীতিটিকে নতুন করে ব্যাখ্যা করেন। তাঁর জোর ছিল implementation inheritance নয়, abstraction-এ: client কোড নির্ভর করবে একটি স্থির abstract টাইপের (interface) ওপর, আর নতুন আচরণ আসবে নতুন implementation হিসেবে — ঠিক এই পর্বের checkout আর Payment-এর মতো। পরে এই নীতি SOLID নামের পাঁচ নীতির “O” হিসেবে বিখ্যাত হয়; SOLID সংক্ষিপ্ত নামটি ২০০০-এর দশকের শুরুতে Michael Feathers-এর দেওয়া বলে সাধারণত উল্লেখ করা হয়। আজ যখন কেউ OCP বলেন, প্রায় সবসময় Martin-এর এই polymorphism-ভিত্তিক অর্থই বোঝান।

কেন কাজের। যে কোড বদলায় না, তাতে নতুন bug ঢোকে না, আর তার টেস্টও আবার লিখতে হয় না। বড় দলে একজন নতুন পেমেন্ট গেটওয়ে যোগ করছেন, আরেকজন checkout-এর UI বদলাচ্ছেন — দুজনের কোড একে অপরের ঘাড়ে পড়ে না। Java-র নিজের API-তেও এটা দেখা যায়: Comparator, Runnable বা List interface না বদলেই লক্ষ লক্ষ নতুন implementation লেখা হয়েছে।

অতিরিক্ত প্রয়োগের বিপদ। প্রতিটি জিনিসের জন্য আগে থেকে interface আর ভবিষ্যতের অজানা টাইপের জন্য abstraction বানালে কোড ভারী, পরোক্ষ আর পড়া কঠিন হয় — এমন abstraction যাকে কোনোদিন দ্বিতীয় implementation পায়নি। Extreme Programming আন্দোলনের “YAGNI” (You Aren’t Gonna Need It) ঠিক এর বিরুদ্ধে সতর্ক করে। ব্যবহারিক পরামর্শ: প্রথমবার সরল কোড লিখুন; যখন বাস্তবে দ্বিতীয় বা তৃতীয় রকম আসে আর একই if-শিকল দু-তিন জায়গায় দেখা দেয়, তখন polymorphism-এ ভাঙুন। কোন দিকে পরিবর্তন আসবে, তা অনুমান নয়, অভিজ্ঞতা বলে দেয়।

মূল উপলব্ধি: Open/Closed কোনো আইন নয়, একটি লক্ষ্য — যে দিকে সিস্টেম সত্যিই বাড়ে, সেদিকে পুরোনো কোড না ছুঁয়ে বাড়ার সুযোগ রাখা। Polymorphism সেই লক্ষ্যে পৌঁছানোর Java-র প্রধান হাতিয়ার।

অনুশীলন

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

  1. পুরোনো কোড না ছুঁয়ে নতুন টাইপ: অংশ ৪-এর প্রোগ্রামে একটি RocketPayment যোগ করুন, যার চার্জ ১.৮% আর নাম “Rocket”. লুপের কোনো লাইন বদলাবেন না — শুধু নতুন class আর array-তে একটি object. মোট চার্জ কত হবে?
    class RocketPayment extends Payment {
        @Override String name() { return "Rocket"; }
        @Override double fee(double amount) { return amount * 0.018; }
    }
    // main-এ: { new BkashPayment(), new NagadPayment(),
    //           new CardPayment(), new RocketPayment() }

    নতুন লাইন Rocket fee: 18.00, আর মোট 69.00 (51.00 + 18.00). লুপ, printf, total — কিছুই বদলায়নি। এটাই Open/Closed: সিস্টেম বাড়ল, পুরোনো কোড বন্ধই থাকল। @Override বাদ দিয়ে ভুল করে double fee(int amount) লিখে দেখুন — তখন এটি নতুন overload, override নয়; লুপ fee(1000) ডাকে fee(double) signature দিয়ে (static type Payment-এ ওটাই আছে), তাই Rocket-এর চার্জ আসবে 0.00 — নীরব bug, যা @Override থাকলে compile error হয়ে ধরা পড়ত।

  2. আউটপুট অনুমান করুন: অংশ ৭-এর প্রোগ্রামে Account a = new SavingsAccount(); বদলে SavingsAccount a = new SavingsAccount(); করুন, আর শেষের if ব্লকটি মুছে দিন। প্রথম তিনটি println এখন কী ছাপবে? না চালিয়ে বলুন, তারপর মিলিয়ে নিন।

    savings account, Savings, Bank S. প্রথমটি বদলায়নি — method সবসময় runtime type দেখে, আর object আগেও SavingsAccount ছিল। দ্বিতীয় আর তৃতীয়টি বদলেছে, কারণ field আর static method static type দেখে, আর static type এখন SavingsAccount. Object একটুও বদলায়নি, বদলেছে শুধু ভ্যারিয়েবলের ঘোষিত টাইপ — অথচ আউটপুট বদলে গেল। ঠিক এই কারণেই field hiding আর object দিয়ে static method ডাকা বিভ্রান্তিকর।

  3. শিকল ভাঙুন: একটি OTP সার্ভিসে কোড আছে — if (n instanceof SmsNotifier) cost = 0.25; else if (n instanceof EmailNotifier) cost = 0.0; else if (n instanceof PushNotifier) cost = 0.01;. কাগজে polymorphic নকশা আঁকুন: কোন method কোথায় যাবে, আর WhatsApp চ্যানেল যোগ করতে কী কী বদলাতে হবে?

    Base class Notifier-এ একটি method double cost() রাখুন (পরের পর্বে শিখবেন একে abstract করতে)। SmsNotifier override করে ফেরত দেয় 0.25, EmailNotifier 0.0, PushNotifier 0.01. ব্যবহারকারী কোড হয় শুধু cost = n.cost();. WhatsApp যোগ করতে একটি নতুন class WhatsAppNotifier extends Notifier, তাতে cost() override — পুরোনো কোনো ফাইল খুলতে হয় না। একই সাথে send(msg)-ও প্রতিটি class-এ রাখলে পাঠানোর নিয়মও নিজ নিজ class-এ থাকবে। (সংখ্যাগুলো উদাহরণ, আসল দর নয়।)

  4. Compile, crash, না চলবে? Payment p = new CardPayment(); ধরে নিচের তিনটি লাইনের প্রতিটির জন্য বলুন: compile error, চলার সময় exception, নাকি ঠিকঠাক চলবে — (ক) BkashPayment b = (BkashPayment) p; (খ) String s = (String) p; (গ) if (p instanceof BkashPayment b) System.out.println(b.fee(100));

    (ক) compile হয়, কারণ Payment থেকে BkashPayment-এ cast সম্ভব; কিন্তু চলার সময় object আসলে CardPayment, তাই ClassCastException. (খ) compile error — Payment আর String-এর মধ্যে কোনো উত্তরাধিকার সম্পর্ক নেই (আর String একটি final class), তাই এই cast কখনোই সফল হতে পারে না, compiler সেটা আগেই বোঝে: incompatible types: Payment cannot be converted to String. (গ) ঠিকঠাক চলে, কিছুই ছাপে না — পরীক্ষা মিথ্যা, তাই ভেতরের লাইন চলে না, আর কোনো exception-ও হয় না। নিরাপদ downcast-এর সঠিক রূপ এটাই।

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

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

ক্রমPayment p = new BkashPayment(); p.fee(1000); — এই ডাক থেকে ফল পাওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔍 javac দেখে static type Payment-এ fee(double) আছে কি না
  2. 📝 javac bytecode-এ signature-সহ একটি invokevirtual নির্দেশ লেখে
  3. 📦 চলার সময় JVM p থেকে object-টি খুঁজে তার class পড়ে: BkashPayment
  4. 📋 JVM BkashPayment-এর vtable থেকে fee(double)-এর ঘরের কোড বের করে
  5. ✅ BkashPayment.fee চলে আর 18.5 ফেরত দেয়

class A { String f() { return "A"; } } আর class B extends A { @Override String f() { return "B"; } } — A x = new B(); System.out.println(x.f()); কী ছাপবে?

  • A
  • B
  • compile error
  • ClassCastException

Account a = new SavingsAccount(); যেখানে interest() method শুধু SavingsAccount-এ আছে। a.interest() লিখলে কী হবে?

  • ঠিকঠাক চলবে, কারণ object আসলে SavingsAccount
  • চলার সময় ClassCastException
  • compile error: cannot find symbol
  • 0.0 ফেরত দেবে

একটি class-এ static void show(Object o) আর static void show(String s) আছে। Object o = "Hi"; show(o); — কোনটি চলবে?

  • show(Object)
  • show(String)
  • compile error: ambiguous
  • JVM চলার সময় ঠিক করবে

Java-তে নিচের কোন ধরনের method dynamic dispatch (runtime polymorphism) পায়?

  • static method
  • private method
  • final method
  • সাধারণ (non-static, non-private, non-final) instance method

Payment p = new NagadPayment(); BkashPayment b = (BkashPayment) p; — যেখানে NagadPayment আর BkashPayment দুটিই Payment-এর subclass. কী ঘটবে?

  • compile error
  • compile হবে, কিন্তু চলার সময় ClassCastException
  • ঠিকঠাক চলবে, b দিয়ে Bkash-এর হিসাব হবে
  • b-তে null বসবে

class P { String n = "P"; } আর class C extends P { String n = "C"; } — P x = new C(); System.out.println(x.n); কী ছাপবে?

  • C
  • compile error
  • P
  • null

শব্দকোষ

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

মনে রাখুন

  1. Polymorphism = এক ডাক, অনেক আচরণ। ব্যবহারকারী কোড শুধু base type জানে; কোন সংস্করণ চলবে, তা আসল object ঠিক করে।
  2. Static type ঠিক করে কী ডাকা যাবে, runtime type ঠিক করে কোনটা চলবে। Upcasting নিজে থেকে ও নিরাপদ; subclass-এর বিশেষ method তখন সরাসরি দেখা যায় না।
  3. Java-তে static, private, final ছাড়া সব instance method virtual; JVM object-এর class-এর vtable থেকে কোড খোঁজে, আর JIT প্রায়ই সেটাকে সরাসরি ডাকে বদলে দেয়।
  4. Overload বাছে argument-এর static type (compile-এর সময়), override বাছে receiver-এর runtime type (চলার সময়)। Field আর static method polymorphic নয়; constructor থেকে override-যোগ্য method ডাকবেন না।
  5. Downcast-এর আগে instanceof — Java 16 থেকে a instanceof SavingsAccount s; ভুল cast-এ ClassCastException. instanceof-শিকল দেখলে polymorphism-এ ভাঙুন (Open/Closed).

পরের পর্বে: এই পর্বের Payment-এর fee() ফেরত দিত 0 — একটি অর্থহীন “ডিফল্ট”. পরের পর্বে দেখবেন abstract class আর interface দিয়ে কীভাবে বলা যায় “প্রতিটি পেমেন্টের fee() থাকতেই হবে, কিন্তু base-এ কোনো হিসাব নেই” — আর কেন আধুনিক Java-তে polymorphism-এর ভিত্তি সাধারণত interface.

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

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