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

Java inheritance — extends আর super

Java Inheritance — extends, super, Method Overriding, @Override and Composition over Inheritance

Taught in Bangla, this lesson covers inheritance: when an is-a relationship justifies extends, what is and isn't inherited, protected access, constructor chaining with super(...), overriding with @Override and its rules, final and method hiding, the Object root class, and why composition often beats inheritance, shown through the fragile base class problem.

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

  • কখন inheritance ঠিক (“is-a”), extends-এর syntax, আর কী inherit হয়, কী হয় না
  • protected-এর আসল অর্থ, আর protected field কেন বিপজ্জনক
  • Constructor chaining, super(...), লুকানো super() আর তার আসল compile error
  • Method overriding, @Override, super.method(), overriding-এর নিয়ম, final আর method hiding
  • Object class, inheritance hierarchy, আর কেন “composition over inheritance” — fragile base class সমস্যা হাতে-কলমে

১ · Inheritance — একটি class আরেকটির বিশেষ ধরন

পর্ব ১৫ থেকে ১৮-এ আপনি class লিখেছেন, constructor দিয়েছেন, field private রেখেছেন। এবার একটি ব্যাংকের সফটওয়্যার ভাবুন। সব account-এর কিছু জিনিস একই — account নম্বর, মালিকের নাম, ব্যালান্স, টাকা জমা, টাকা তোলা। কিন্তু savings account-এ বছর শেষে সুদ যোগ হয়, আর current account-এ ব্যবসায়ীরা ব্যালান্সের চেয়ে কিছু বেশি টাকা তুলতে পারেন (overdraft). দুটি আলাদা class লিখে সাধারণ অংশটি দুবার কপি করবেন? তাহলে জমার নিয়ম বদলালে দুই জায়গায় বদলাতে হবে — পর্ব ১২-এর সেই পুরোনো সমস্যা, এবার class-এর স্তরে।

Java-র সমাধান inheritance. সাধারণ অংশ একবার লিখুন একটি class-এ — Account. তারপর বলুন: “SavingsAccount হলো এক ধরনের Account, সাথে সুদের হিসাব।” যে class থেকে পাওয়া হয়, সে superclass (parent); যে পায়, সে subclass (child). Subclass parent-এর field আর method আপনাআপনি পায়, নিজের নতুন জিনিস যোগ করে, আর চাইলে parent-এর কোনো আচরণ নিজের মতো বদলায়।

বাংলাদেশের রাস্তার যানবাহন ভাবুন। “যানবাহন” বললেই কিছু জিনিস বোঝায় — চাকা আছে, যাত্রী ওঠে, ভাড়া আছে, চলে আর থামে। বাস এক ধরনের যানবাহন, সাথে রুট আর অনেক আসন। সিএনজি অটোরিকশাও এক ধরনের যানবাহন, সাথে মিটার আর তিন চাকা। “বাস কি যানবাহন?” — হ্যাঁ। “সিএনজি কি যানবাহন?” — হ্যাঁ। এই “হ্যাঁ, এটা ওটারই এক ধরন” প্রশ্নটিই inheritance-এর পরীক্ষা। কিন্তু “বাস কি চাকা?” — না, বাসের চাকা আছে। সেটা অন্য সম্পর্ক, অংশ ৮-এ দেখবেন।

পর্ব ০২-এ এই দুই সম্পর্কের নাম শুনেছিলেন: is-a (একটি আরেকটির বিশেষ ধরন) আর has-a (একটি আরেকটিকে অংশ হিসেবে রাখে)। Inheritance শুধু প্রথমটির জন্য। এই পর্বে তিনটি বাস্তব উদাহরণ বারবার আসবে:

Account → SavingsAccount, CurrentAccount

সবার ব্যালান্স, জমা, তোলা আছে। Savings সুদ যোগ করে; Current ব্যালান্সের চেয়ে বেশি তুলতে দেয় (overdraft সীমা পর্যন্ত)।

যোগ ও বদল

Employee → Manager

সবার নাম আর মূল বেতন আছে। Manager-এর বেতনের হিসাবে বোনাস যোগ হয় — parent-এর হিসাব রেখে তার ওপর।

super.method()

Vehicle → Bus, Cng

সবার রেজিস্ট্রেশন নম্বর আর ভাড়ার হিসাব আছে। বাসের ভাড়া স্টপেজ ধরে, সিএনজির ভাড়া মিটার ধরে।

একাধিক স্তর

UML-এ inheritance আঁকা

কোড লেখার আগে দলগুলো প্রায়ই ছবি আঁকে। Class আর তাদের সম্পর্ক আঁকার মানক ভাষা UML class diagram. Inheritance বোঝাতে child থেকে parent-এর দিকে একটি তীর আঁকা হয়, যার মাথা ফাঁপা ত্রিভুজ। তীরের দিক খেয়াল করুন: child জানে তার parent কে, parent জানে না তার কয়টি child আছে।

একটি ছোট class hierarchy

UML: hollow-triangle arrows point from subclass to superclass

                 ┌──────────────┐
                 │    Object    │   (java.lang — সবার চূড়ায়)
                 └──────▲───────┘
                        │
              ┌─────────┴─────────┐
              │      Account      │
              │ - accountNo       │   - মানে private
              │ # balance         │   # মানে protected
              │ + deposit()       │   + মানে public
              │ + withdraw()      │
              └───▲───────────▲───┘
                  │           │
   ┌──────────────┴──┐   ┌────┴──────────────┐
   │ SavingsAccount  │   │  CurrentAccount   │
   │ - rate          │   │ - overdraftLimit  │
   │ + addInterest() │   │ + withdraw()  ⟲   │   ⟲ = override
   └─────────────────┘   └───────────────────┘

এই গাছকে বলে class hierarchy. ওপরে সাধারণ, নিচে বিশেষ। প্রতিটি নিচের class ওপরের সব কিছু পায়, তাই একটি SavingsAccount object-এর কাছে deposit() আছে (Account থেকে), addInterest() আছে (নিজের), আর toString()-ও আছে (Object থেকে, অংশ ৭)।

Inheritance-এর ধারণা এসেছে Simula 67 ভাষা থেকে, যা নরওয়ের Ole-Johan Dahl আর Kristen Nygaard তৈরি করেছিলেন সিমুলেশন লেখার জন্য। সেখানেই প্রথম class আর subclass ছিল। পরে Smalltalk আর C++ ধারণাটিকে জনপ্রিয় করে, আর ১৯৯৫ সালে Java এসে C++-এর অনেক জটিলতা ছেঁটে একটি সরল সংস্করণ রাখে — যার সবচেয়ে বড় ছাঁটাই ছিল multiple inheritance (অংশ ২ আর ভাবনার প্রশ্ন ১)।

২ · extends — কী উত্তরাধিকারে আসে, কী আসে না

Syntax একটি শব্দের: class-এর নামের পরে extends আর parent-এর নাম।

class Account {
    private final String accountNo;
    protected double balance;
    Account(String accountNo, double opening) {
        this.accountNo = accountNo;
        this.balance = opening;
    }
    public String getAccountNo() { return accountNo; }
    public void deposit(double amt) { balance += amt; }
}

class SavingsAccount extends Account {
    private final double rate;                 // শুধু SavingsAccount-এর
    SavingsAccount(String no, double opening, double rate) {
        super(no, opening);                    // অংশ ৪-এ বিস্তারিত
        this.rate = rate;
    }
    public void addInterest() { balance += balance * rate; }
}

SavingsAccount-এর ভেতরে deposit বা getAccountNo লেখা নেই, তবু s.deposit(500) আর s.getAccountNo() দুটোই চলে। কিন্তু “পায়” কথাটির সূক্ষ্ম অর্থ আছে। নিচের টেবিলটিই এই অংশের সারকথা:

Parent-এর জিনিসSubclass কী পায়
public আর protected field ও methodপায় এবং সরাসরি নাম ধরে ব্যবহার করতে পারে
কিছু না লেখা (package-private) memberপায়, কিন্তু ব্যবহার করতে পারে শুধু একই package-এ থাকলে
private fieldobject-এর ভেতরে আছে, কিন্তু subclass-এর কোড নাম ধরে ছুঁতে পারে না — parent-এর public/protected method দিয়ে পরোক্ষে
private methodSubclass দেখতেই পায় না, তাই override-ও করতে পারে না
Constructorinherit হয় না — প্রতিটি class নিজের constructor লেখে, আর super(...) দিয়ে parent-েরটি ডাকে
static field ও methodSubclass-এর নাম দিয়েও ডাকা যায়, কিন্তু override হয় না (অংশ ৬-এ hiding)

তৃতীয় সারিটি সবচেয়ে বেশি ভুল বোঝা হয়। “Private field কি inherit হয়?” — ইন্টারভিউতে এর সঠিক উত্তর দুই ভাগে: জায়গা হিসেবে আছে, নাম হিসেবে নেই। Java Language Specification-এর ভাষায় private member subclass-এ “inherit হয় না” — অর্থাৎ নাম ধরে পাওয়া যায় না। কিন্তু প্রতিটি SavingsAccount object-এর ভেতরে accountNo-এর জন্য জায়গা ঠিকই আছে, কারণ Account-এর method-গুলো সেটা ব্যবহার করে। Subclass থেকে সরাসরি ছুঁতে গেলে javac বলে:

Main.java:5: error: accountNo has private access in Account
        return "No: " + accountNo;
                        ^

নিচের ডায়াগ্রামে একটি SavingsAccount object-এর ভেতরটা ধাপে ধাপে দেখুন:

ধাপ ১new Account("A-101", 5000)
heap
┌─────────────── Account object ───────────────┐
│ accountNo = "A-101"     (private)            │
│ balance   = 5000.0      (protected)          │
└──────────────────────────────────────────────┘
শুধু parent-এর field
ধাপ ২new SavingsAccount("S-202", 10000, 0.05)
heap
┌──────────── একটিই SavingsAccount object ────────────┐
│ ┌── Account অংশ ──────────────────────────────┐     │
│ │ accountNo = "S-202"     (private)           │     │
│ │ balance   = 10000.0     (protected)         │     │
│ └─────────────────────────────────────────────┘     │
│   rate      = 0.05        (SavingsAccount-এর নিজের) │
└─────────────────────────────────────────────────────┘
দুটি object নয় — একটি object, ভেতরে দুই স্তর
ধাপ ৩SavingsAccount-এর কোড কোনটা ছুঁতে পারে
SavingsAccount-এর method থেকে:
  balance        ✔  protected — সরাসরি
  rate           ✔  নিজের field
  deposit(...)   ✔  public method, Account থেকে পাওয়া
  getAccountNo() ✔  public method দিয়ে accountNo পড়া
  accountNo      ✘  private — "has private access in Account"
জায়গা আছে, কিন্তু চাবি parent-এর কাছে
ধাপ ৪পাশের ভাই — new CurrentAccount("C-303", 2000, 5000)
heap
┌──────────── CurrentAccount object ──────────────────┐
│ ┌── Account অংশ ─────────────────────────────┐      │
│ │ accountNo = "C-303"                         │      │
│ │ balance   = 2000.0                          │      │
│ └─────────────────────────────────────────────┘      │
│   overdraftLimit = 5000.0    (নিজের)                │
└─────────────────────────────────────────────────────┘
rate এখানে নেই — ভাইবোন একে অপরের কিছু পায় না
  1. সাধারণ Account. একটি Account object-এ দুটি field: private accountNo আর protected balance. এগুলোর জায়গা তৈরি হয় heap-এ, new-এর সময়।
  2. SavingsAccount — একটি object, দুই স্তর। অনেকে ভাবেন subclass-এর object বানালে ভেতরে আলাদা একটি Account object-ও তৈরি হয়। না — heap-এ একটিই object, যার ভেতরে আগে Account-এর অংশ, তারপর SavingsAccount-এর নিজের rate. এজন্যই একে Account হিসেবেও ব্যবহার করা যায় (পর্ব ২০)।
  3. জায়গা থাকা আর ছুঁতে পারা আলাদা। accountNo object-এর ভেতরেই আছে, কিন্তু private হওয়ায় শুধু Account-এর নিজের কোড ছুঁতে পারে। SavingsAccount পড়তে চাইলে getAccountNo() ডাকে। এটাই encapsulation — inheritance তা ভাঙে না।
  4. ভাইবোন আলাদা। CurrentAccount-ও Account অংশ পায়, সাথে নিজের overdraftLimit. কিন্তু SavingsAccount-এর rate তার কাছে নেই। Inheritance শুধু ওপর থেকে নিচে নামে, পাশে যায় না।

একটিই parent — single inheritance

একটি class কি দুটি class extends করতে পারে? ধরুন class StudentEmployee extends Student, Employee. Java-তে এটা syntax-স্তরেই অসম্ভব:

Main.java:3: error: '{' expected
class C extends A, B { }
                 ^

Compiler extends A-এর পরে সরাসরি { আশা করে — কমা দিয়ে দ্বিতীয় নামের কোনো জায়গাই রাখা হয়নি। Java class-এর ক্ষেত্রে single inheritance মানে। কারণটি হলো diamond problem: Student আর Employee দুজনেই যদি Person থেকে আসে আর দুজনেই getId() নিজের মতো লেখে, তাহলে StudentEmployee-এর getId() কোনটা? Person-এর name field কি এক কপি, না দুই কপি? C++ এর উত্তর দেয় virtual inheritance-এর মতো জটিল নিয়ম দিয়ে; Java প্রশ্নটাই বাদ দিয়েছে। “একাধিক ধরন” প্রকাশ করার দরকার হলে Java-তে আছে interface — একটি class অনেকগুলো interface implement করতে পারে, কারণ interface-এ object-এর অবস্থা (field) থাকে না। সেটা পর্ব ২১-এ।

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

C-তে inheritance নেই — struct-এর প্রথম সদস্য হিসেবে আরেকটি struct রেখে কাছাকাছি কিছু করা হয়। C++-এ class Savings : public Account, আর multiple inheritance চলে; Java-তে একটিই parent, সব method নিজে থেকেই override করা যায় (C++-এর মতো virtual লিখতে হয় না), আর object থাকে heap-এ, তাই object slicing-এর সমস্যাও নেই। Python-এ class Savings(Account), multiple inheritance চলে (MRO নিয়মে), আর parent-এর __init__ ডাকতে হয় super().__init__() দিয়ে — Java-তে super(...) না লিখলেও compiler একটি বসিয়ে দেয়। তুলনা করতে দেখুন C++-এর inheritance আর Python-এর inheritance ও composition.

৩ · protected — পরিবারের জন্য খোলা দরজা

পর্ব ১৭-এ চারটি access level দেখেছেন। Inheritance-এ এসে protected-এর অর্থ পরিষ্কার হয়। Java-তে protected-এর অর্থ অনেকে যা ভাবেন তার চেয়ে বেশি খোলা:

Access modifierএকই classএকই packageঅন্য package-এর subclassসবাই
private✔✘✘✘
(কিছু না লেখা)✔✔✘✘
protected✔✔✔✘
public✔✔✔✔

খেয়াল করুন দ্বিতীয় কলাম: protected member একই package-এর সব class ব্যবহার করতে পারে, subclass হোক বা না হোক। C++-এর protected শুধু subclass-এর জন্য; Java-রটা “package + subclass”. তাই protected মানে “শুধু পরিবারের জন্য” নয় — “পরিবার আর পুরো পাড়া”।

Protected field — সুবিধা না ফাঁদ?

অংশ ২-এর কোডে balance protected রাখা হয়েছিল, যাতে SavingsAccount সরাসরি balance += … লিখতে পারে। সুবিধাজনক, কিন্তু ভাবুন: Account-এর একটি নিয়ম (invariant) আছে — “ব্যালান্স কখনো ঋণাত্মক হবে না”। withdraw() এটা পাহারা দেয়। এখন যে কেউ একটি subclass লিখে balance = -1_000_000; করতে পারে, কোনো পাহারা ছাড়াই। আর একই package-এর যেকোনো class-ও পারে। একটি protected field মানে আপনার class-এর নিয়ম এখন অসংখ্য অজানা class-এর সততার ওপর নির্ভর করছে।

class Account {
    private double balance;                       // field private-ই থাকুক
    protected void addToBalance(double amt) {     // subclass-এর জন্য নিয়ন্ত্রিত দরজা
        if (balance + amt < 0) throw new IllegalStateException("negative balance");
        balance += amt;
    }
    public double getBalance() { return balance; }
}
মূল ধারণা

সাধারণ নিয়ম: field private, subclass-এর দরকারি কাজের জন্য protected method. Method নিয়ম যাচাই করতে পারে, field পারে না। Protected field চলে তখনই, যখন parent আর সব subclass একই দলের হাতে, একই package-এ, আর কোনো নিয়ম ভাঙার ঝুঁকি নেই — এই পর্বের ছোট উদাহরণগুলোর মতো।

৪ · Constructor আর super(...) — আগে parent, পরে child

Subclass parent-এর method পায়, কিন্তু constructor পায় না। কারণ সহজ: constructor-এর কাজ তার নিজের class-এর field গোছানো। Account-এর constructor জানে না SavingsAccount-এর rate আছে। তাই প্রতিটি class নিজের constructor লেখে। কিন্তু Account অংশটি (অংশ ২-এর ধাপ ২) কে গোছাবে? Account নিজেই — আর সেজন্য Java-র কড়া নিয়ম:

প্রতিটি constructor-এর প্রথম কাজ
parent-এর একটি constructor ডাকা।

এই ডাক লেখা হয় super(...) দিয়ে। Java 21-এ এটি অবশ্যই constructor-এর প্রথম statement হতে হবে। আগে অন্য কিছু লিখলে:

Main.java:7: error: call to super must be first statement in constructor
        super(opening);
             ^

যুক্তিটা হলো: child-এর কোড চলার আগে parent অংশ সম্পূর্ণ তৈরি থাকতে হবে, যাতে child নিশ্চিন্তে parent-এর method ডাকতে পারে। (Java 22–24-এ preview হিসেবে পরীক্ষার পর Java 25-এ JEP 513 এই নিয়ম কিছুটা শিথিল করেছে — super(...)-এর আগে argument যাচাইয়ের মতো কিছু statement লেখা যায়, তবে তখনো তৈরি হতে থাকা object-টিকে (যেমন তার method ডাকা) ব্যবহার করা যায় না। এই কোর্সে আমরা Java 21 ধরে চলি, যেখানে পুরোনো নিয়মই খাটে।)

লুকানো super() — আর তার বিখ্যাত error

আপনি super(...) না লিখলে compiler নিজে প্রথম লাইনে super(); বসিয়ে দেয় — parameter-হীন parent constructor-এর ডাক। আর কোনো constructor-ই না লিখলে compiler একটি default constructor বানায়, যার ভেতরে শুধু ওই super();. সমস্যা হয় তখন, যখন parent-এ parameter-হীন constructor নেই:

class Account {
    protected double balance;
    Account(double opening) { balance = opening; }   // শুধু এটাই
}
class SavingsAccount extends Account {
    SavingsAccount() {                                // এখানে লুকানো super();
        System.out.println("savings");
    }
}
Main.java:6: error: constructor Account in class Account cannot be applied to given types;
    SavingsAccount() {
                     ^
  required: double
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error

Error-টি পড়তে শিখুন, কারণ নতুনদের কাছে এটা রহস্যময় লাগে — আপনি তো Account(...) কোথাও ডাকেননি! ডেকেছে compiler, লুকিয়ে: super(). মেসেজ বলছে, Account-এর constructor চায় একটি double (required), কিন্তু পেয়েছে কিছুই না (found: no arguments). Subclass-এ কোনো constructor না লিখলেও একই error আসে, তখন তীরচিহ্ন class-এর লাইনে। সমাধান দুটির একটি: subclass-এর constructor-এ নিজে super(0) বা super(opening) লিখুন, অথবা — যদি অর্থপূর্ণ হয় — Account-এ একটি parameter-হীন constructor যোগ করুন। ব্যাংক account-এর ক্ষেত্রে প্রথমটিই ঠিক: শুরুর ব্যালান্স ছাড়া account খোলার মানে নেই।

এবার সব একসাথে চালিয়ে দেখুন — constructor-এর ক্রম, inherited method, নিজের method আর override:

class Account {
    protected double balance;
    Account(double opening) { balance = opening; System.out.println("1. Account"); }
    void deposit(double amt) { balance += amt; }
    String info() { return "Balance: " + balance; }
}
class SavingsAccount extends Account {
    private final double rate;
    SavingsAccount(double opening, double rate) {
        super(opening);                  // সবার আগে parent অংশ
        this.rate = rate; System.out.println("2. SavingsAccount");
    }
    void addInterest() { balance += balance * rate; }
    @Override String info() { return super.info() + " (savings)"; }
}
class Main {
    public static void main(String[] args) {
        var s = new SavingsAccount(10000, 0.05);
        s.deposit(2000);  s.addInterest();   // প্রথমটি Account-এর, দ্বিতীয়টি নিজের
        System.out.println(s.info());
    }
}

আউটপুট: 1. Account, 2. SavingsAccount, Balance: 12600.0 (savings). আমরা new SavingsAccount লিখেছি, অথচ আগে ছাপা হলো Account — কারণ SavingsAccount-এর constructor-এর প্রথম লাইন super(opening) আগে parent-এর constructor পুরোটা চালায়, তারপর নিজের বাকি লাইন। s.deposit(2000) SavingsAccount-এ লেখা নেই, Account থেকে পাওয়া; ব্যালান্স হলো 12000. addInterest() সরাসরি protected balance ছোঁয়: 12000 + 600 = 12600. শেষে info() — SavingsAccount এটি override করেছে, কিন্তু ভেতরে super.info() দিয়ে Account-এর লেখাটুকু নিয়ে তার পরে “(savings)” জুড়েছে (অংশ ৫)। Class-গুলো public ছাড়া লেখা, কারণ অনলাইন runner ফাইলটিকে অন্য নামে compile করে; নিজের প্রজেক্টে সাধারণত প্রতিটি public class নিজের ফাইলে থাকে।

Hierarchy তিন স্তরের হলে কী হয়? প্রতিটি constructor ওপরেরটিকে ডাকে, সেটি আবার তার ওপরেরটিকে — শেষে Object-এ গিয়ে থামে। একে বলে constructor chaining. ডাক যায় নিচ থেকে ওপরে, কিন্তু শরীর চলে ওপর থেকে নিচে। ধাপে ধাপে দেখুন:

1class Vehicle {2  Vehicle(String route) {3    // লুকানো super() → Object()4    System.out.println("Vehicle " + route);5  }6}7class Bus extends Vehicle {8  Bus(String route) {9    super(route);10    System.out.println("Bus");11  }12}13class AcBus extends Bus {14  AcBus() {15    super("Dhaka-Sylhet");16    System.out.println("AcBus");17  }18}19new AcBus();   // main-এ
অবস্থা
এখন চলছে
অপেক্ষায়
  1. new AcBus(). JVM আগে পুরো object-এর জন্য heap-এ জায়গা নেয় — Vehicle, Bus, AcBus তিন স্তরের সব field-সহ — আর সব field-এ ডিফল্ট মান (0, null) বসায়। তারপর constructor ডাকে।
  2. AcBus() ঢুকল, কিন্তু নিজের কাজ শুরু করল না। প্রথম লাইনই super("Dhaka-Sylhet") — আগে parent.
  3. Bus(route) ঢুকল। তারও প্রথম লাইন super(route). এখন AcBus আর Bus দুজনেই অপেক্ষায়, কেউ কিছু ছাপেনি।
  4. Vehicle(route) — আর তারও ওপরে। Vehicle-এ super লেখা নেই, তাই compiler লুকিয়ে super() বসিয়েছে, যা Object() ডাকে। Object-এর constructor কিছু করে না, চুপচাপ ফেরে। এটাই chain-এর চূড়া।
  5. শরীর চলা শুরু — ওপর থেকে। প্রথমে Vehicle-এর শরীর: ছাপে Vehicle Dhaka-Sylhet.
  6. Bus-এর শরীর। Vehicle শেষ, ফিরে এসে Bus-এর super(route)-এর পরের লাইন: Bus.
  7. সবশেষে AcBus. ছাপে AcBus. ডাক গেছে নিচ থেকে ওপরে (AcBus → Bus → Vehicle → Object), কাজ হয়েছে ওপর থেকে নিচে (Object → Vehicle → Bus → AcBus) — বাড়ি বানানোর মতো, আগে ভিত, পরে ছাদ। OpenJDK 21-এ এই তিন স্তরের ক্রম চালিয়ে যাচাই করা।

Constructor থেকে override করা যায় এমন method ডাকবেন না। Parent-এর constructor যদি এমন একটি method ডাকে যা child override করেছে, তাহলে child-এর সংস্করণটি চলে — কিন্তু তখনো child-এর constructor চলেনি, তার field-গুলো ডিফল্ট মানে (0 বা null). ফলে অদ্ভুত ফল বা NullPointerException. Effective Java-এর পরামর্শ: constructor থেকে শুধু private, final বা static method ডাকুন।

৫ · Method overriding আর @Override

CurrentAccount-এ টাকা তোলার নিয়ম আলাদা: ব্যালান্সের চেয়ে বেশি, overdraft সীমা পর্যন্ত তোলা যায়। Account-এর withdraw এখানে ভুল আচরণ করবে। সমাধান — CurrentAccount-এ একই method হুবহু একই signature-এ নতুন করে লেখা। একে বলে method overriding:

class Account {
    protected double balance;
    Account(double opening) { balance = opening; }
    public boolean withdraw(double amt) {
        if (amt > balance) return false;          // ব্যালান্সের বেশি নয়
        balance -= amt;
        return true;
    }
}
class CurrentAccount extends Account {
    private final double overdraftLimit;
    CurrentAccount(double opening, double limit) { super(opening); overdraftLimit = limit; }

    @Override
    public boolean withdraw(double amt) {
        if (amt > balance + overdraftLimit) return false;   // সীমা পর্যন্ত চলবে
        balance -= amt;
        return true;
    }
}

এখন new CurrentAccount(2000, 5000).withdraw(6000) চলে (ব্যালান্স হয় −4000), কিন্তু একটি সাধারণ Account বা SavingsAccount-এ একই ডাক false দেয়। একই নামের ডাক, object অনুযায়ী আলাদা কাজ — এটাই polymorphism-এর বীজ, পরের পর্বে যার পুরো গল্প।

Overloading (পর্ব ১২) থেকে পার্থক্য মনে রাখুন: overloading হলো ভিন্ন parameter-তালিকায় একই নাম, overriding হলো হুবহু একই method signature — নাম আর parameter টাইপ — subclass-এ।

Method খোঁজা হয় কীভাবে

s.deposit(…) ডাকলে JVM বোঝে কীভাবে কোন কোড চালাতে হবে? নিয়মটি সহজ: object-এর আসল class থেকে খোঁজ শুরু, না পেলে এক ধাপ ওপরে, তারপর আরও ওপরে — Object পর্যন্ত। প্রথম যেখানে পাওয়া যায়, সেটাই চলে। নিচে একটি SavingsAccount object-এ পাঁচটি ডাক:

1class Object {  // সরলীকৃত2  String toString() { … }3}4class Account {5  void deposit(double a) { … }6  String info() { … }7}8class SavingsAccount extends Account {9  void addInterest() { … }10  @Override String info() {11    return super.info() + "…"; }12}
খোঁজের পথ
ডাক
খোঁজ শুরু
পাওয়া গেল
  1. s.addInterest(). খোঁজ শুরু object-এর আসল class SavingsAccount থেকে। সেখানেই আছে — শেষ।
  2. s.deposit(500). SavingsAccount-এ নেই। এক ধাপ ওপরে Account — পাওয়া গেল। Inherited method এভাবেই “পাওয়া” যায়: কোড কপি হয় না, খোঁজ ওপরে যায়।
  3. s.info() — override. SavingsAccount-এ info() আছে, তাই খোঁজ সেখানেই থামে। Account-এর info() মুছে যায়নি, শুধু এই object-এর জন্য আড়ালে পড়েছে।
  4. super.info() — খোঁজ এক ধাপ ওপর থেকে। SavingsAccount-এর ভেতর থেকে super.info() বললে খোঁজ শুরু হয় parent থেকে, নিজের class বাদ দিয়ে। তাই Account-এর সংস্করণ চলে — override করা method থেকে parent-এর কাজটুকু ব্যবহারের উপায় এটাই।
  5. s.toString(). নিচের দুই class-এ নেই, Object-এ আছে। প্রতিটি Java object-এর কাছে তাই toString, equals, hashCode থাকে (অংশ ৭)।
  6. s.fly(). কোথাও নেই। এটা চলার সময় ধরা পড়ে না — compiler আগেই cannot find symbol দেয়, কারণ compile-এর সময় সে hierarchy দেখে নিশ্চিত হয় method-টি আছে কি না। বাস্তবে JVM প্রতিবার ওপরে হাঁটে না; প্রতিটি class-এর জন্য আগেই একটি method-টেবিল বানিয়ে রাখে, কিন্তু ফল এই খোঁজের মতোই।

super.method() — বদলানো নয়, বাড়ানো

অনেক সময় parent-এর কাজ পুরো বাদ দিতে চান না, তার ওপর কিছু যোগ করতে চান। Manager-এর বেতন = সাধারণ কর্মীর বেতন + বোনাস:

class Employee {
    private final double basic;
    Employee(double basic) { this.basic = basic; }
    public double monthlySalary() { return basic + basic * 0.5; }   // মূল + ভাতা
}
class Manager extends Employee {
    private final double bonus;
    Manager(double basic, double bonus) { super(basic); this.bonus = bonus; }
    @Override
    public double monthlySalary() { return super.monthlySalary() + bonus; }
}

ভাতার নিয়ম বদলালে Employee-তে একবার বদলালেই Manager-ও নতুন হিসাব পাবে, কারণ Manager হিসাবটি কপি করেনি, super.monthlySalary() দিয়ে ডেকেছে। খেয়াল করুন, Manager basic সরাসরি ছোঁয় না — সেটা private, আর দরকারও নেই।

@Override — এক লাইনের বিমা

ওপরের সব override-এ @Override লেখা। এটি একটি annotation — compiler-এর জন্য একটি ঘোষণা: “আমি parent-এর কোনো method override করছি বলে দাবি করছি, মিলিয়ে দেখো।” বাধ্যতামূলক নয়, তবু কেন প্রতিবার লিখবেন, নিচের ঘটনাটিতে দেখুন:

উদ্দেশ্যBus-এর জন্য সুন্দর লেখা
class Bus extends Vehicle {
    private final String route = "Dhaka-Ctg";
    public String tostring() {
        return "Bus: " + route;
    }
}
System.out.println(bus);
আশা: Bus: Dhaka-Ctg
@Override ছাড়াcompile হয়, চলে — ভুল ফল
$ javac Main.java      (কোনো error, কোনো warning নেই)
$ java Main
Bus@4e25154f
tostring() একটি নতুন method — toString()-কে কেউ ছোঁয়নি,
তাই Object-এর ডিফল্ট toString চলল (সংখ্যাটি প্রতিবার আলাদা হতে পারে)
@Override দিয়েcompiler সাথে সাথে ধরে
    @Override
    public String tostring() { ... }

Main.java:3: error: method does not override or implement
a method from a supertype
    @Override
    ^
1 error
ঠিক করার পরেtoString — বড় হাতের S
    @Override
    public String toString() {
        return "Bus: " + route;
    }

$ java Main
Bus: Dhaka-Ctg
  1. উদ্দেশ্য ঠিক, বানান ভুল। প্রোগ্রামার Object-এর toString() override করতে চেয়েছেন, কিন্তু লিখেছেন tostring — ছোট হাতের s. Java case-sensitive, তাই এটা সম্পূর্ণ অন্য নাম।
  2. নীরব ব্যর্থতা। Compiler-এর চোখে কোনো ভুল নেই: Bus একটি নতুন method যোগ করেছে, এটা পুরোপুরি বৈধ। println ভেতরে toString() ডাকে, সেটা Bus-এ নেই, তাই Object-এরটা চলে আর ছাপে class-এর নাম ও একটি hex সংখ্যা। এমন bug ঘণ্টার পর ঘণ্টা খুঁজতে হতে পারে।
  3. @Override যোগ করলেই ধরা। এখন compiler জানে আপনার উদ্দেশ্য override. সে parent-গুলোতে tostring() খোঁজে, পায় না, আর error দেয়। এই আসল মেসেজটি OpenJDK 21-এ যাচাই করা। ভুলটা ধরা পড়ল চলার আগেই, এক সেকেন্ডে।
  4. ঠিক করা হলো। toString() লেখার পর @Override সন্তুষ্ট, আর আউটপুট প্রত্যাশামতো। একই ফাঁদ আরও কয়েকটি রূপে আসে: equals(Bus other) লেখা (parameter টাইপ Object হওয়ার কথা — এটা override নয়, overload), hashcode(), বা parent-এর method-এর নাম পরে বদলে যাওয়া। প্রতিটিতে @Override সাথে সাথে চিৎকার করে।

৬ · Overriding-এর নিয়ম, final আর hiding

Override করা method parent-এর প্রতিশ্রুতি ভাঙতে পারবে না — কারণ যে কোড parent টাইপ নিয়ে কাজ করে, সে child object পেলেও একই আচরণ আশা করে। Java এই প্রতিশ্রুতি compiler দিয়ে পাহারা দেয়। প্রতিটি error নিচে OpenJDK 21-এ যাচাই করা:

নিয়মভাঙলে javac যা বলেকেন
Visibility কমানো যাবে না (public → package-private নয়)attempting to assign weaker access privileges; was publicযে public method সবাই ডাকতে পারত, child-এ সেটা হঠাৎ লুকানো চলবে না
Return type একই, অথবা তার subclassreturn type … is not compatible with …Caller যা পাওয়ার আশা করে, তার চেয়ে “বেশি বিশেষ” কিছু দেওয়া নিরাপদ, ভিন্ন কিছু নয়
নতুন বা বড় checked exception ছোড়া যাবে নাoverridden method does not throw ExceptionCaller যে exception সামলানোর জন্য প্রস্তুত নয়, তা আসতে পারবে না (পর্ব ২৯)
final method override করা যাবে নাoverridden method is finalParent ইচ্ছা করে দরজা বন্ধ করেছে
static method-এ @Override দেওয়া যাবে নাstatic methods cannot be annotated with @OverrideStatic method override হয় না, শুধু hide হয় (নিচে)

দ্বিতীয় সারির ছাড়টির নাম covariant return type (Java 5 থেকে)। Vehicle-এ Vehicle copy() থাকলে Bus সেটাকে Bus copy() দিয়ে override করতে পারে — Bus তো একটি Vehicle-ই, তাই Vehicle চাওয়া caller কোনো অসুবিধায় পড়ে না, আর Bus-এর সাথে কাজ করা caller cast ছাড়াই Bus পায়। Visibility বাড়ানো যায় (protected → public), কমানো যায় না — একই যুক্তিতে।

final — “এখানেই শেষ”

পর্ব ১৮-এ final ভ্যারিয়েবল দেখেছেন। একই শব্দ method আর class-এও বসে, অর্থ একই — আর বদলানো যাবে না:

  • final method — কোনো subclass override করতে পারবে না। যেমন Account-এর final boolean verifyPin(...): নিরাপত্তা-যাচাইয়ের নিয়ম কোনো subclass নিজের মতো ঢিলে করতে পারবে না।
  • final class — কেউ extends-ই করতে পারবে না। Java-র String, Integer আর অন্য wrapper class, Math সবাই final. String final না হলে কেউ একটি “String” subclass বানিয়ে immutability ভেঙে দিতে পারত — আর String-এর ওপর নির্ভর করা নিরাপত্তা-কোড (ফাইলের path, class-এর নাম) বিপদে পড়ত। চেষ্টা করলে: error: cannot inherit from final Account.

পর্ব ২৫-এর record সবসময় final, আর পর্ব ২৪-এর enum-কেও বাইরের কোনো class extends করতে পারে না। আর পর্ব ২৬-এ দেখবেন sealed — “সবার জন্য বন্ধ নয়, শুধু এই কয়েকটি নির্দিষ্ট class extends করতে পারবে” — final আর খোলা class-এর মাঝামাঝি একটি পথ।

Overriding, overloading আর hiding — তিন ভাই

class Vehicle {
    static String type() { return "Vehicle"; }
    String name = "vehicle";
    String name() { return name; }
}
class Bus extends Vehicle {
    static String type() { return "Bus"; }      // hiding, override নয়
    String name = "bus";                         // field-ও শুধু আড়াল করে
    @Override String name() { return name; }     // আসল override
}

Vehicle v = new Bus();
v.name();     // "bus"      — instance method: object-এর আসল class দেখে
v.name;       // "vehicle"  — field: ভ্যারিয়েবলের ঘোষিত টাইপ দেখে
Vehicle.type() + " " + Bus.type();   // "Vehicle Bus"

এখানে v-এর ঘোষিত টাইপ Vehicle, কিন্তু ভেতরে আছে একটি Bus. Instance method override হয়, তাই চলার সময় আসল object (Bus) দেখে Bus-এর name() চলে। কিন্তু field আর static method polymorphic নয় — এগুলো compile-এর সময় ঘোষিত টাইপ দেখে ঠিক হয়। Subclass-এ একই নামের static method লিখলে তাকে বলে method hiding; একই নামের field লিখলে field hiding. দুটিই বিভ্রান্তিকর, বাস্তব কোডে এড়িয়ে চলুন — static method সবসময় class-এর নাম দিয়ে ডাকুন (Bus.type()), আর subclass-এ parent-এর field-এর নাম আবার ব্যবহার করবেন না। (পুরো উদাহরণটি চালিয়ে আউটপুট যাচাই করা।)

OverridingOverloadingHiding
কোথায়Subclass-এএকই বা subclass-এSubclass-এ
Signatureহুবহু একইParameter আলাদাহুবহু একই (static) বা একই নামের field
কিসের জন্যInstance methodযেকোনো methodStatic method, field
কখন ঠিক হয় কোনটা চলবেচলার সময়, object দেখেCompile-এর সময়, argument দেখেCompile-এর সময়, ঘোষিত টাইপ দেখে

৭ · Object — সবার পূর্বপুরুষ, আর hierarchy কত গভীর হবে

যে class-এ extends লেখা নেই, সেটাও আসলে একটি subclass. Compiler নীরবে ধরে নেয় extends Object. java.lang.Object তাই Java-র সব class-এর মূল — String, ArrayList, আপনার Account, সবার। এজন্য যেকোনো object-এর কাছে কয়েকটি method সবসময় থাকে:

  • toString() — object-এর লেখারূপ; ডিফল্টে class-এর নাম আর একটি hex সংখ্যা, যা প্রায় কোনো কাজের নয়, তাই প্রায় সব class এটা override করে।
  • equals(Object) আর hashCode() — দুটি object “সমান” কি না; ডিফল্টে ==-এর মতো, অর্থাৎ একই object কি না।
  • getClass() — object-এর আসল class (এটা final, override করা যায় না)।

এগুলো কীভাবে ঠিকমতো override করতে হয় — বিশেষ করে equals আর hashCode-এর জোড়া চুক্তি — পর্ব ২৩-এর পুরো বিষয়। আপাতত মনে রাখুন: System.out.println(anyObject) কাজ করে কারণ প্রতিটি object-এর কাছে Object থেকে পাওয়া toString() আছে। আর new Bus() instanceof Object সবসময় true.

কত স্তর পর্যন্ত গভীর?

Inheritance শিখে অনেকের প্রথম ঝোঁক হয় সব কিছুকে একটি বিশাল গাছে সাজানো: Vehicle → MotorVehicle → PassengerVehicle → Bus → AcBus → VolvoAcBus… প্রতিটি স্তর বোঝার জন্য তার ওপরের সব স্তর বুঝতে হয়, আর একটি method কোথায় লেখা আছে তা খুঁজতে পাঁচটি ফাইল খুলতে হয়। ওপরের কোনো class-এ একটি বদল নিচের সবগুলোকে প্রভাবিত করে। বাস্তব অভিজ্ঞতা থেকে একটি মোটামুটি নিয়ম: নিজের কোডে দুই-তিন স্তরের বেশি গভীর hierarchy হলে থেমে ভাবুন। প্রায়ই দেখা যায় কিছু “স্তর” আসলে একটি field (যেমন hasAc) বা একটি আলাদা object (composition) হওয়া উচিত ছিল।

পরীক্ষা ও ইন্টারভিউতে
  • Java কি multiple inheritance সমর্থন করে? — Class-এর ক্ষেত্রে না (diamond problem এড়াতে); একটি class একাধিক interface implement করতে পারে।
  • Constructor কি inherit হয়? override করা যায়? — কোনোটাই না; subclass super(...) দিয়ে parent-এর constructor ডাকে, আর তা প্রথম statement হতে হয়।
  • Private method override করা যায়? — না; subclass সেটা দেখতেই পায় না, একই নামে লিখলে সেটা একটি নতুন, সম্পর্কহীন method.
  • Static method override করা যায়? — না; একই signature-এ লিখলে hiding হয়, কোনটা চলবে তা ঘোষিত টাইপ দেখে compile-এর সময় ঠিক হয়।
  • super আর this-এর পার্থক্য? — this বর্তমান object বা একই class-এর আরেকটি constructor; super parent-এর constructor বা parent-এর সংস্করণের method/field.

৮ · Composition over inheritance — কখন extends করবেন না

Inheritance শক্তিশালী, কিন্তু এর একটি লুকানো দাম আছে: subclass parent-এর ভেতরের কাজের ধরনের সাথে বাঁধা পড়ে যায়। Parent কোন method ভেতরে কোন method ডাকে, সেটা parent-এর ব্যক্তিগত ব্যাপার — কিন্তু subclass সেটার ওপর নির্ভর করে ফেলে, প্রায়ই না জেনে। নিচের উদাহরণটি Joshua Bloch-এর Effective Java বইয়ের একটি বিখ্যাত উদাহরণের সরল রূপ। লক্ষ্য: একটি set-এ মোট কতবার কিছু যোগ করার চেষ্টা হয়েছে, গোনা।

import java.util.*;
class CountingSet extends HashSet<String> {          // inheritance
    int added = 0;
    @Override public boolean add(String s) { added++; return super.add(s); }
    @Override public boolean addAll(Collection<? extends String> c) {
        added += c.size(); return super.addAll(c);
    }
}
class CountingSet2 {                                  // composition
    private final Set<String> set = new HashSet<>();
    int added = 0;
    boolean add(String s) { added++; return set.add(s); }
    boolean addAll(Collection<String> c) { added += c.size(); return set.addAll(c); }
}
class Main {
    public static void main(String[] args) {
        var a = new CountingSet();  a.addAll(List.of("Dhaka", "Khulna", "Sylhet"));
        var b = new CountingSet2(); b.addAll(List.of("Dhaka", "Khulna", "Sylhet"));
        System.out.println("extends HashSet : added = " + a.added);
        System.out.println("has-a HashSet   : added = " + b.added);
    }
}

আউটপুট: extends HashSet : added = 6, has-a HashSet : added = 3. তিনটি শহর যোগ করেছি, অথচ inheritance সংস্করণ বলছে ছয়! কারণ: CountingSet.addAll প্রথমে 3 যোগ করল, তারপর super.addAll(c) ডাকল। HashSet-এর addAll (আসলে তার parent AbstractCollection থেকে পাওয়া) ভেতরে প্রতিটি element-এর জন্য add() ডাকে — আর অংশ ৫-এর method lookup অনুযায়ী সেই add() হলো আমাদের override করা সংস্করণ, যা আবার একবার করে গুনল। 3 + 3 = 6. HashSet-এর ডকুমেন্টেশনে এই ভেতরের আচরণের প্রতিশ্রুতি নেই; ভবিষ্যতের কোনো Java সংস্করণে বদলালে আমাদের গণনা আবার অন্যরকম ভুল হবে। Composition সংস্করণ HashSet-কে একটি private field হিসেবে রাখে আর কাজ পাঠিয়ে দেয়; ভেতরে HashSet কী করে, তাতে তার কিছু যায় আসে না। (List.of পর্ব ৩০-এ, generics-এর <String> পর্ব ২৮-এ বিস্তারিত।)

এই ঘটনার নাম fragile base class problem: parent class-এর এমন বদল বা ভেতরের আচরণ, যা তার নিজের জন্য সম্পূর্ণ ঠিক, কিন্তু subclass ভেঙে দেয়। আর সমাধানের নাম composition: extends না করে, অন্য object-কে field হিসেবে রাখুন (has-a), আর তার কাজ ব্যবহার করুন। CountingSet2-এর add আর addAll হলো forwarding method — কাজটা ভেতরের set-কে পাঠিয়ে দেয়, সাথে নিজের গণনা যোগ করে। এই “মুড়ে দিয়ে ক্ষমতা যোগ” নকশাটি পর্ব ০৯-এ দেখা decorator pattern-এর আত্মীয়।

JDK-র নিজের ভুল: Stack extends Vector

Java-র নির্মাতারাও এই ফাঁদে পড়েছিলেন। ১৯৯৬ সালের JDK 1.0-তে java.util.Stack লেখা হয় Vector-এর subclass হিসেবে — কোড পুনর্ব্যবহারের সহজ পথ। কিন্তু stack মানে শুধু ওপরে রাখা আর ওপর থেকে তোলা (LIFO); Vector হলো যেকোনো জায়গায় ঢোকানো-বের করা যায় এমন তালিকা। Inheritance-এর ফলে Stack-এর ওপর Vector-এর সব public method চলে আসে: stack.add(0, x) দিয়ে stack-এর তলায় ঢোকানো যায়, stack.get(3) দিয়ে মাঝখান থেকে পড়া যায়। “Stack is-a Vector” — সত্যি নয়। একই ভুল Properties extends Hashtable-এ। আজ Stack-এর Javadoc নিজেই বলে LIFO-র জন্য Deque interface আর তার implementation (যেমন ArrayDeque) ব্যবহার করতে (পর্ব ৩২)। কিন্তু Stack মুছে ফেলা যায় না — লাখো পুরোনো প্রোগ্রাম এর ওপর নির্ভর করে। Inheritance-এর ভুল সংশোধন করা কঠিন, কারণ এটা একটি public প্রতিশ্রুতি।

কখন extends, কখন composition — একটি ছোট পরীক্ষা

৯ · AI-এর যুগে — inheritance গাছ ছাঁটা আপনার কাজ

AI-কে “একটি বাস কোম্পানির টিকিট সিস্টেম Java-তে বানিয়ে দাও” বললে প্রায়ই সুন্দর দেখতে একটি বিশাল class গাছ আসে — BaseEntity → AbstractVehicle → MotorVehicle → Bus → AcBus — প্রতিটি স্তরে কয়েকটি protected field. দেখতে “পেশাদার”, কিন্তু প্রথম বদলেই ভাঙে। AI ইন্টারনেটের লাখো টিউটোরিয়াল থেকে শিখেছে, আর পুরোনো টিউটোরিয়ালে inheritance-কে প্রায়ই কোড পুনর্ব্যবহারের প্রধান উপায় হিসেবে দেখানো হতো।

AI যা করেকেন সমস্যাআপনি যা করবেন
চার-পাঁচ স্তরের গভীর hierarchyবোঝা কঠিন, ওপরের বদল নিচে অজানা ভাঙন আনে“দুই স্তরের বেশি নয়, প্রতিটি স্তরের কারণ বলো” — শর্ত দিন
শুধু কোড পুনর্ব্যবহারের জন্য extends (যেমন OrderService extends DatabaseHelper)is-a সম্পর্ক নেই, অপ্রয়োজনীয় method বাইরে চলে আসেhas-a হলে composition চান: helper-কে field হিসেবে নিতে বলুন
@Override বাদ দেয়বানান বা parameter-এর ভুল নীরবে overload হয়ে যায়প্রতিটি override-এ @Override বাধ্যতামূলক করতে বলুন
সব field protectedSubclass আর পুরো package parent-এর নিয়ম ভাঙতে পারেField private, দরকারি কাজ protected method — চান
Parent-এ parameter-হীন constructor না দেখে subclass লেখে“cannot be applied to given types” compile errorError মেসেজটি হুবহু দিয়ে বলুন subclass-এ সঠিক super(...) বসাতে

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

আমি Java 21 শিখছি, inheritance আর composition-এর পার্থক্য বুঝি। নিচের সিস্টেমের জন্য class নকশা করে দাও: [যেমন — ব্যাংকের Savings আর Current account, বা বাস ও সিএনজির ভাড়া হিসাব] শর্ত: ১. extends ব্যবহার করবে শুধু সত্যিকারের is-a সম্পর্কে; প্রতিটি extends-এর পাশে এক লাইনে লেখো "X is a Y" কেন সত্য। ২. শুধু কোড পুনর্ব্যবহারের জন্য inheritance নয় — has-a হলে composition (field) ব্যবহার করো। ৩. hierarchy দুই স্তরের বেশি গভীর হবে না (Object বাদে)। ৪. সব field private; subclass-এর দরকার হলে protected method দাও, protected field নয়। ৫. প্রতিটি override-এ @Override দাও; parent-এর কাজ রাখতে হলে super.method() ব্যবহার করো। ৬. প্রতিটি constructor-এ super(...) স্পষ্ট লেখো। ৭. যে class extends করার জন্য নয়, তাকে final করো, আর কারণ বলো। ৮. শেষে বলো: কোন জায়গায় subclass বসালে parent-এর কোনো প্রতিশ্রুতি ভাঙতে পারে (Liskov).

AI-এর লেখা class গাছ হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১C++-এ multiple inheritance আছে। Java-র নির্মাতারা ইচ্ছা করে এটা বাদ দিলেন কেন — আর Java 8-এর default method কি সেই দরজা আবার খুলে দিল?

১৯৯০-এর দশকের শুরুতে Sun Microsystems-এ James Gosling-এর দল যখন Java নকশা করছিল, তাঁদের সামনে ছিল C++-এর অভিজ্ঞতা। C++ শক্তিশালী, কিন্তু জটিল; আর তার সবচেয়ে বিতর্কিত বৈশিষ্ট্যগুলোর একটি ছিল multiple inheritance. Java-র শুরুর দিকের white paper-এ (Gosling আর Henry McGilton-এর লেখা “The Java Language Environment”) স্পষ্ট বলা হয়েছিল, C++-এর যেসব বৈশিষ্ট্য সুবিধার চেয়ে বেশি ঝামেলা দেয়, সেগুলো বাদ দেওয়া হয়েছে — operator overloading আর multiple inheritance সেই তালিকায় ছিল।

Diamond-এর আসল সমস্যা। ধরুন Person-এর একটি name field আর describe() method আছে। Student আর Employee দুজনেই Person extends করে, দুজনেই describe() override করে। এখন StudentEmployee দুজনকেই extends করলে দুটি প্রশ্ন ওঠে। এক, describe() ডাকলে কোনটা চলবে? দুই, object-এর ভেতরে Person অংশ কয় কপি — একটি, না দুটি? C++ দ্বিতীয় প্রশ্নের উত্তর দেয় virtual inheritance দিয়ে, যেখানে প্রোগ্রামারকে আগেই ঠিক করতে হয় Person অংশ ভাগাভাগি হবে কি না; আর তখন constructor ডাকার নিয়মও জটিল হয়ে যায় — সবচেয়ে নিচের class-কে সরাসরি Person-এর constructor ডাকতে হয়। প্রথম প্রশ্নে C++ ambiguity error দেয়, আর প্রোগ্রামারকে Student::describe() লিখে বেছে নিতে হয়।

Java-র ভাগ: টাইপ বনাম অবস্থা। Java মূল সমস্যাটিকে চিহ্নিত করেছিল অবস্থা (field) আর বাস্তবায়ন-এর একাধিক উৎস হিসেবে। তাই সিদ্ধান্ত: class থেকে (যেখানে field আর কোড থাকে) একটিই parent; কিন্তু interface থেকে (যেখানে শুরুতে শুধু method-এর নাম, কোনো কোড বা field ছিল না) যত খুশি। ফলে একটি class একাধিক “টাইপ” হতে পারে — Comparable, Serializable, Runnable একসাথে — কিন্তু diamond-এর দুটি প্রশ্নের কোনোটাই ওঠে না।

Java 8-এর মোড়। ২০১৪ সালে Java 8 interface-এ default method আনে — interface-এর ভেতরে কোডসহ method. কারণ ছিল ব্যবহারিক: Collection interface-এ stream()-এর মতো নতুন method যোগ করতে হবে, অথচ লাখো পুরোনো implementation ভাঙা চলবে না। এর ফলে আচরণের multiple inheritance সীমিতভাবে ফিরে এল। কিন্তু Java স্পষ্ট নিয়ম দিল: class-এর নিজের বা superclass-এর method সবসময় জেতে; তারপর বেশি নির্দিষ্ট interface জেতে; তাতেও দুটি সমান দাবিদার থাকলে compile error, আর class-কে নিজে override করে A.super.describe() লিখে বেছে নিতে হয়। আর field-এর diamond এখনো অসম্ভব, কারণ interface-এ instance field নেই। পর্ব ২১-এ এটা বিস্তারিত দেখবেন।

মূল উপলব্ধি: Java multiple inheritance পুরো নিষিদ্ধ করেনি, ভাগ করেছে — টাইপের বহুত্ব হ্যাঁ, অবস্থার বহুত্ব না। সমস্যার আসল উৎস চিহ্নিত করে শুধু সেটুকু কেটে ফেলাই ভালো ভাষা-নকশা।

প্র ০২java.util.Stack extends Vector, আর Properties extends Hashtable — JDK-র নিজের এই inheritance ভুলগুলো কী শেখায়? এগুলো ঠিক করা হয় না কেন?

১৯৯৬ সালের JDK 1.0 লেখা হয়েছিল খুব দ্রুত, আর তখন object-oriented নকশার অনেক শিক্ষা এখনকার মতো পরিষ্কার ছিল না। সেই সময়ের দুটি class আজ পাঠ্যবইয়ের “কী করবেন না” উদাহরণ।

Stack extends Vector. Stack-এর ধারণা কঠোর: শুধু push, pop, peek — ওপরে রাখো, ওপর থেকে তোলো। কিন্তু Vector extends করায় Stack-এর ওপর Vector-এর সব public method চলে এল: add(int, E), remove(int), get(int), insertElementAt… যেকোনো কোড একটি Stack নিয়ে তার মাঝখানে বা তলায় কিছু ঢোকাতে পারে, আর Stack-এর LIFO প্রতিশ্রুতি এক লাইনে ভেঙে যায়। আরও মজার ব্যাপার: Stack-কে for-each দিয়ে হাঁটলে উপাদান আসে তলা থেকে ওপরে (Vector-এর ক্রমে), stack-এর স্বাভাবিক ক্রমে নয়। আজ Stack-এর Javadoc নিজেই বলে, LIFO stack-এর জন্য আরও সম্পূর্ণ ও সামঞ্জস্যপূর্ণ ব্যবস্থা হলো Deque interface আর তার implementation — যেমন ArrayDeque.

Properties extends Hashtable. Properties হলো configuration — key আর value দুটোই String. কিন্তু Hashtable<Object, Object> extends করায় put(Object, Object) উন্মুক্ত। যে কেউ props.put("port", 8080) লিখতে পারে — value একটি Integer. তারপর getProperty("port") ফেরত দেয় null, কারণ getProperty শুধু String value খোঁজে। Properties-এর Javadoc-এ লেখা আছে, যেহেতু Properties Hashtable থেকে inherit করে, তাই put আর putAll ব্যবহার করা যায়, কিন্তু তা দৃঢ়ভাবে নিরুৎসাহিত করা হয়, কারণ এতে String নয় এমন key বা value ঢুকে পড়তে পারে।

ঠিক করা যায় না কেন। Java-র সবচেয়ে বড় প্রতিশ্রুতি হলো backward compatibility — পুরোনো কোড নতুন JVM-এ চলবে। “Stack is a Vector” একটি public ঘোষণা; কেউ না কেউ নিশ্চয়ই কোথাও Stack-কে Vector হিসেবে method-এ পাঠিয়েছে বা stack.get(i) লিখেছে। Superclass বদলালে সেই সব কোড ভাঙবে। তাই JDK শুধু নতুন ও ভালো বিকল্প যোগ করেছে (Java 6-এ ArrayDeque) আর পুরোনোটিকে ডকুমেন্টেশনে নিরুৎসাহিত করেছে। Joshua Bloch — যিনি পরে Java Collections Framework নকশা করেন — Effective Java-তে ঠিক এই দুটি class উদাহরণ হিসেবে দেখিয়েছেন: এখানে composition ব্যবহার করা উচিত ছিল।

মূল উপলব্ধি: extends একটি public, স্থায়ী প্রতিশ্রুতি — parent-এর পুরো interface আপনার interface হয়ে যায়। Composition-এর ভুল ভেতরে ঠিক করা যায়; inheritance-এর ভুল দশকের পর দশক টিকে থাকে।

প্র ০৩“Favor composition over inheritance” — এই উপদেশ কোথা থেকে এলো? Inheritance যদি এত বিপজ্জনক হয়, তবে ভাষায় রাখা হলো কেন?

বাক্যটি বিখ্যাত হয় ১৯৯৪ সালে প্রকাশিত Design Patterns: Elements of Reusable Object-Oriented Software বই থেকে, যার চার লেখক Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides — “Gang of Four” নামে পরিচিত। বইয়ের শুরুর অধ্যায়ে তাঁরা object-oriented নকশার দুটি নীতি দেন, যার দ্বিতীয়টি: class inheritance-এর চেয়ে object composition-কে অগ্রাধিকার দাও।

White-box বনাম black-box. GoF পার্থক্যটা দুটি সুন্দর নামে ব্যাখ্যা করেছিলেন। Inheritance হলো “white-box reuse” — subclass parent-এর ভেতরটা দেখে, তার ভেতরের কাজের ধরনের ওপর নির্ভর করে; তাঁদের ভাষায় inheritance encapsulation ভাঙে। Composition হলো “black-box reuse” — আপনি শুধু object-এর public interface ব্যবহার করেন, ভেতরে কী হয় জানার দরকার নেই। এই পর্বের CountingSet ঠিক এটাই দেখায়: inheritance সংস্করণ HashSet-এর ভেতরের একটি বিবরণে (addAll ভেতরে add ডাকে) আটকে গেছে।

Effective Java-র ভূমিকা। Joshua Bloch-এর Effective Java (প্রথম সংস্করণ ২০০১; তৃতীয় সংস্করণ ২০১৮) Java জগতে উপদেশটিকে প্রায় আইনে পরিণত করে। তৃতীয় সংস্করণের Item 18 হলো “Favor composition over inheritance” — সেখানেই InstrumentedHashSet উদাহরণ, যার সরল রূপ এই পর্বে দেখলেন। ঠিক পরের Item 19: “Design and document for inheritance or else prohibit it” — অর্থাৎ হয় class-টিকে inheritance-এর জন্য যত্ন করে নকশা ও ডকুমেন্ট করুন (কোন method ভেতরে কোনটাকে ডাকে তা লিখে দিন), নয়তো final করে দিন।

তাহলে inheritance রাখা কেন? কারণ ঠিক জায়গায় এটা অসাধারণ। যখন সম্পর্ক সত্যিই is-a, parent আর child একই দলের হাতে, আর parent-কে inheritance-এর জন্য নকশা করা হয়েছে — যেমন একটি framework-এর abstract class, যার কিছু নির্দিষ্ট method subclass-কে পূরণ করতে বলা হয় — তখন inheritance পরিষ্কার আর কম কোডে কাজ করে। সমস্যা inheritance নয়, অপব্যবহার: অন্যের class extends করা, শুধু কোড পুনর্ব্যবহারের জন্য extends করা। Java-র পরবর্তী ভাষাগুলোও এই শিক্ষা নিয়েছে — Kotlin-এ class ডিফল্টে final, extends করতে দিতে হলে open লিখতে হয়; Go আর Rust-এ class inheritance-ই নেই, composition আর interface/trait দিয়ে কাজ চলে।

মূল উপলব্ধি: Inheritance সবচেয়ে শক্ত বন্ধন — তাই ডিফল্ট হাতিয়ার হিসেবে composition নিন, আর inheritance রাখুন সেই জায়গার জন্য, যেখানে is-a সম্পর্ক সত্যি এবং parent সেটার জন্য তৈরি।

প্র ০৪Square কি একটি Rectangle? গণিতে হ্যাঁ — কিন্তু কোডে class Square extends Rectangle লিখলে কেন বিপদ? Liskov Substitution Principle কী বলে?

১৯৮৭ সালে OOPSLA সম্মেলনের একটি keynote বক্তৃতায় (“Data Abstraction and Hierarchy”) MIT-এর অধ্যাপক Barbara Liskov subtype-এর একটি আচরণগত সংজ্ঞা দেন। সহজ ভাষায়: যে প্রোগ্রাম T টাইপের object নিয়ে ঠিকভাবে চলে, তাতে T-এর জায়গায় S টাইপের object বসালেও প্রোগ্রামের আচরণ ঠিক থাকলে, তবেই S হলো T-এর subtype. পরে ১৯৯৪ সালে Jeannette Wing-এর সাথে তিনি এর আনুষ্ঠানিক রূপ প্রকাশ করেন। আজ এর নাম Liskov Substitution Principle — SOLID নীতিমালার “L”. Liskov ২০০৮ সালে ডেটা অ্যাবস্ট্রাকশন ও প্রোগ্রামিং ভাষায় অবদানের জন্য Turing Award পান।

Square-Rectangle সমস্যা। Rectangle-এ আছে setWidth(w), setHeight(h), area(). Square তো এক ধরনের rectangle, তাই Square extends Rectangle — আর বর্গের দুই বাহু সমান রাখতে Square দুটি setter override করে, যাতে একটি বদলালে অন্যটিও বদলায়। এখন কেউ একটি সাধারণ method লিখলেন: void resize(Rectangle r) { r.setWidth(5); r.setHeight(4); assert r.area() == 20; }. Rectangle পাঠালে ক্ষেত্রফল 20. Square পাঠালে? setHeight(4) প্রস্থও 4 করে দিল, ক্ষেত্রফল 16. Method-টি কোনো ভুল করেনি — Rectangle-এর প্রতিশ্রুতি অনুযায়ী প্রস্থ আর উচ্চতা স্বাধীন। Square সেই প্রতিশ্রুতি ভেঙেছে। কোড compile হয়, চলে — কিন্তু ভুল ফল দেয়।

শিক্ষাটা কী। “is-a” প্রশ্নটি বাস্তব জগতের শ্রেণিবিভাগ নিয়ে নয়, আচরণের প্রতিশ্রুতি নিয়ে। গণিতে বর্গ একটি আয়তক্ষেত্র, কারণ গণিতের আকার বদলায় না। কিন্তু পরিবর্তনযোগ্য (mutable) Rectangle-এর একটি প্রতিশ্রুতি আছে, যা Square রাখতে পারে না। সমাধানের কয়েকটি পথ: আকারগুলোকে immutable করুন (setter নেই, নতুন আকার মানে নতুন object) — তাহলে Square নিরাপদে Rectangle হতে পারে; অথবা দুটিকেই একটি সাধারণ Shape interface-এর ভাইবোন বানান, যেখানে শুধু area() আছে। আধুনিক Java-তে sealed interface Shape permits Circle, Rectangle, Square আর record দিয়ে এটা খুব পরিষ্কারভাবে লেখা যায় — পর্ব ২৫ আর ২৬-এ দেখবেন।

Java-র নিজের নিয়মগুলোও LSP-র প্রয়োগ। অংশ ৬-এর overriding-এর নিয়ম — visibility কমানো যাবে না, return type আরও নির্দিষ্ট হতে পারে কিন্তু ভিন্ন নয়, নতুন checked exception নয় — এগুলো compiler দিয়ে LSP-র যতটুকু যাচাই করা যায়, ততটুকু। কিন্তু “Square-এর setHeight প্রস্থও বদলায়” — এমন আচরণগত ভাঙন কোনো compiler ধরতে পারে না। সেটা ধরা আপনার নকশা-বুদ্ধি আর টেস্টের কাজ।

মূল উপলব্ধি: subclass মানে শুধু “বাস্তবে এক ধরনের” নয়, “parent-এর প্রতিটি প্রতিশ্রুতি রক্ষা করে”। প্রতিশ্রুতি রাখতে না পারলে, যত স্বাভাবিকই মনে হোক, extends ভুল।

প্র ০৫@Override এলো Java 5-এ, ২০০৪ সালে। এত উপকারী হলে এটা বাধ্যতামূলক নয় কেন — যেখানে C# আর Kotlin-এ override শব্দ লেখা বাধ্যতামূলক?

Java-র প্রথম আট-নয় বছর কোনো annotation ছিল না। Override হতো নীরবে: subclass-এ একই signature-এর method লিখলেই সেটা override, ভুল হলে নীরবে নতুন method. এই পর্বের tostring ঘটনার মতো bug তখন সাধারণ ব্যাপার ছিল, বিশেষ করে equals(MyClass o) — যা আসলে overload, override নয়, আর HashMap-এর মতো collection-এ object খুঁজে না পাওয়ার রহস্যময় bug তৈরি করত।

Java 5-এর annotation বিপ্লব। ২০০৪ সালে Java 5 (JSR 175-এর মাধ্যমে) annotation নিয়ে আসে, আর সাথে আসে তিনটি built-in annotation: @Override, @Deprecated, @SuppressWarnings. @Override-এর retention হলো SOURCE — অর্থাৎ এটা শুধু compiler-এর জন্য; compile-এর পর class ফাইলে এর কোনো চিহ্ন থাকে না, চলার গতিতে কোনো প্রভাব নেই। একটি ছোট মজার ইতিহাস: Java 5-এ interface-এর method implement করার সময় @Override দিলে javac error দিত; Java 6 থেকে সেটাও অনুমোদিত হয়, তাই পুরোনো কিছু কোডে interface method-এর ওপর @Override না দেখলে অবাক হবেন না।

বাধ্যতামূলক নয় কেন। উত্তর এক শব্দে: backward compatibility. ২০০৪ সালের মধ্যেই কোটি কোটি লাইনের Java কোড ছিল, যেখানে override-এর পাশে কোনো চিহ্ন নেই। @Override বাধ্যতামূলক করলে রাতারাতি সেই সব কোড compile হওয়া বন্ধ হয়ে যেত। Java-র দর্শনে এটা অগ্রহণযোগ্য। C#-এর অবস্থা ছিল আলাদা — ২০০০ সালে নতুন ভাষা হিসেবে শুরু থেকেই virtual আর override শব্দ বাধ্যতামূলক রাখতে পেরেছিল। Kotlin-ও নতুন ভাষা, তাই সেখানে override modifier না লিখলে compile error. C++ একই সমস্যায় পড়েছিল — C++11-এ এসে override যোগ হয়, কিন্তু Java-র মতোই ঐচ্ছিক, একই কারণে।

বাস্তবে ফাঁক কীভাবে বন্ধ হয়। ভাষা বাধ্য না করলেও টুলগুলো করে। IntelliJ IDEA আর অন্যান্য IDE override-এর পাশে @Override না থাকলে সতর্ক করতে পারে এবং নিজে থেকে যোগ করে দেয়; Google-এর Error Prone-এর মতো static analysis টুলেও এর জন্য check আছে, আর বহু প্রতিষ্ঠানের coding standard-এ @Override বাধ্যতামূলক। অর্থাৎ ভাষা পুরোনো কোডকে সম্মান করেছে, আর নতুন কোডের শৃঙ্খলা দিয়েছে টুল আর দলের নিয়ম।

মূল উপলব্ধি: একটি জনপ্রিয় ভাষা নতুন নিরাপত্তা যোগ করতে পারে, কিন্তু পুরোনো কোড ভাঙতে পারে না — তাই অনেক ভালো অভ্যাস “ঐচ্ছিক কিন্তু সবাই করে” হিসেবে আসে। @Override সেই অভ্যাসগুলোর একটি; প্রতিটি override-এ লিখুন।

অনুশীলন

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

  1. Constructor ভাঙুন, তারপর সারান: অংশ ৪-এর চালানো যায় এমন প্রোগ্রামে SavingsAccount-এর constructor থেকে super(opening); লাইনটি মুছে দিন। না চালিয়ে বলুন কী হবে, তারপর চালিয়ে মিলিয়ে নিন। এরপর Account-এ কী যোগ করলে আবার compile হবে — আর সেটা কি ভালো সমাধান?

    Compile error: constructor Account in class Account cannot be applied to given types; সাথে required: double, found: no arguments. লাইনটি মুছলে compiler লুকিয়ে super() বসায়, কিন্তু Account-এ parameter-হীন constructor নেই। Account-এ Account() { } যোগ করলে compile হবে, কিন্তু তখন SavingsAccount-এর ব্যালান্স 0.0 থেকে শুরু হবে — opening-এর 10000 কোথাও বসবে না, আর আউটপুট হবে Balance: 2100.0 (savings) (2000 জমা, তারপর ৫% সুদ)। Error চলে গেল, কিন্তু bug এলো। ভালো সমাধান হলো super(opening) ফিরিয়ে আনা — compile error-কে চুপ করানোর জন্য নয়, তার কথা বুঝে সারাতে হয়।

  2. @Override সরিয়ে দেখুন: একই প্রোগ্রামে @Override String info()-কে বদলে লিখুন @Override String Info() (বড় হাতের I). কী হয়? এবার @Override মুছে দিন — এখন কী ছাপে, আর কেন?

    @Override থাকলে: method does not override or implement a method from a supertype. মুছে দিলে compile হয়, কিন্তু শেষ লাইনে ছাপে শুধু Balance: 12600.0 — “(savings)” নেই। কারণ Info() একটি নতুন, আলাদা method; s.info() ডাকলে method lookup SavingsAccount-এ info পায় না, Account-এরটা চালায়। একটি অক্ষরের ভুল, কোনো error নেই, শুধু ভুল আউটপুট — @Override-এর মূল্য এটাই।

  3. Construction order অনুমান করুন: Employee আর Manager-এর constructor-এ একটি করে println যোগ করা হলো (Employee-তে “E”, Manager-এ “M”, দুটোই super/field বসানোর পরে)। তার ওপর class SeniorManager extends Manager, যার constructor ছাপে “S”. new SeniorManager(...) কী ছাপবে? আর যদি Employee-তে কোনো constructor লেখা না থাকে, তবু কি Object-এর constructor চলে?

    ছাপবে E, M, S — এই ক্রমে। SeniorManager-এর constructor প্রথমে super(...) দিয়ে Manager-কে ডাকে, Manager ডাকে Employee-কে, Employee (লুকানো super() দিয়ে) Object-কে। শরীর চলে ওপর থেকে নিচে। Employee-তে কোনো constructor না থাকলেও compiler একটি default constructor বানায়, যার ভেতরে super() — তাই হ্যাঁ, Object-এর constructor সবসময় চলে, প্রতিটি object তৈরির সময়। (তবে তখন Manager-কে super() parameter ছাড়া ডাকতে হবে, কারণ Employee-র তখন একটিই, parameter-হীন constructor.)

  4. নকশার সিদ্ধান্ত: নিচের প্রতিটি জোড়ায় extends না composition — এক লাইনে কারণসহ বলুন: (ক) Car আর Engine; (খ) Cng আর Vehicle; (গ) ShoppingCart আর ArrayList<Item>; (ঘ) Manager আর Employee, যেখানে একজন কর্মী কিছুদিন পর manager হতে পারেন আবার সাধারণ পদে ফিরতেও পারেন।

    (ক) Composition — Car has an Engine; গাড়ি ইঞ্জিন নয়, আর ইঞ্জিন বদলানোও যায়। (খ) extends ঠিক — সিএনজি সত্যিই এক ধরনের যানবাহন, আর Vehicle-এর সব আচরণ (ভাড়া, রেজিস্ট্রেশন) তার জন্য অর্থবহ। (গ) Composition — cart-এ ArrayList-এর add(int, E), clear(), sort সব খুলে দেওয়া ঠিক নয়; cart-এর নিজের নিয়ম আছে (মোট দাম, স্টক যাচাই)। ArrayList-কে private field রাখুন — Stack extends Vector-এর ভুল আর না। (ঘ) চমকপ্রদ উত্তর: composition-ই ভালো। Java-তে একটি object-এর class জন্ম থেকে মৃত্যু পর্যন্ত বদলায় না; Employee object-কে Manager বানানো যায় না। তাই “পদ” একটি বদলযোগ্য অংশ হওয়া উচিত — যেমন Employee-এর একটি Role role field. ভূমিকা যদি বদলায়, সেটা is-a নয়, has-a.

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

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

ক্রমclass C extends B, class B extends A — new C() লিখলে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧱 Heap-এ পুরো object-এর জায়গা নেওয়া হয়, সব field ডিফল্ট মানে
  2. 📞 C-এর constructor ডাকা হয়, তার প্রথম লাইন super(...) দিয়ে B-কে ডাকে
  3. 📞 B-এর constructor A-কে, A-এর constructor (লুকানো super()) Object-কে ডাকে
  4. 🅰️ A-এর constructor-এর শরীর চলে
  5. 🅱️ B-এর constructor-এর শরীর চলে
  6. ✅ C-এর constructor-এর শরীর চলে, object ব্যবহারের জন্য তৈরি

Java-তে class X extends A, B { } লিখলে কী ঘটে?

  • X দুটি class-এরই field ও method পায়
  • compile error হয়
  • শুধু প্রথম class A-এর জিনিস পায়, B উপেক্ষিত হয়
  • compile হয়, কিন্তু চলার সময় exception আসে

class A-এর একমাত্র constructor হলো A(int x). class B extends A-তে কোনো constructor লেখা নেই। কী হবে?

  • B ঠিকঠাক compile হবে, A-এর x শূন্য থাকবে
  • B নিজে থেকে A(int x) constructor inherit করবে
  • compile error: constructor A cannot be applied to given types
  • চলার সময় NullPointerException আসবে

Parent class-এ public void show() আছে। Subclass-এ void show() (কোনো access modifier ছাড়া) লিখলে কী হয়?

  • compile error — visibility কমানো যায় না
  • এটি একটি নতুন overloaded method হিসেবে গৃহীত হয়
  • compiler নিজে থেকে একে public বানিয়ে নেয়
  • compile হয়, কিন্তু বাইরে থেকে ডাকলে চলার সময় error

Java-তে @Override annotation-এর প্রধান কাজ কী?

  • এটি ছাড়া কোনো method override করা যায় না
  • method-টিকে দ্রুত চালায়
  • subclass-কে আর override করতে দেয় না
  • compiler-কে যাচাই করতে বলে method-টি সত্যিই override করছে কি না

Vehicle v = new Bus(); — Vehicle আর Bus দুটিতেই static String type() আছে, আর instance method String name()-কে Bus override করেছে। v.name() আর v.type() কোন class-এর সংস্করণ চালায়?

  • দুটিই Bus-এর
  • name() Bus-এর, type() Vehicle-এর
  • name() Vehicle-এর, type() Bus-এর
  • দুটিই Vehicle-এর

“Favor composition over inheritance” — নিচের কোন ক্ষেত্রে composition সবচেয়ে উপযুক্ত?

  • Cng আর Vehicle
  • SavingsAccount আর Account
  • Car আর Engine
  • Manager আর Employee (পদ কখনো বদলায় না এমন ব্যবস্থায়)

শব্দকোষ

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

মনে রাখুন

  1. Inheritance শুধু সত্যিকারের is-a সম্পর্কে: class B extends A — B, A-এর public/protected সব পায়, নিজের যোগ করে, কিছু override করে। Class-এর parent একটিই; কিছু না লিখলে Object.
  2. Private field object-এর ভেতরে থাকে, কিন্তু subclass নাম ধরে ছুঁতে পারে না। protected মানে package + subclass; field private রাখুন, protected method দিন।
  3. Constructor inherit হয় না; প্রথম লাইনে super(...), না লিখলে লুকানো super() — parent-এ parameter-হীন constructor না থাকলে “cannot be applied to given types”. Object তৈরি হয় ওপর থেকে নিচে।
  4. Override = হুবহু একই signature, সাথে @Override; super.method() দিয়ে parent-এর কাজ রেখে বাড়ান। Visibility কমানো যায় না, return type আরও নির্দিষ্ট হতে পারে, final বন্ধ করে; static method আর field override হয় না, hide হয়।
  5. Composition over inheritance: extends মানে parent-এর ভেতরের আচরণের সাথে স্থায়ী বন্ধন (fragile base class, Stack extends Vector). সন্দেহ হলে has-a — field রেখে কাজ পাঠিয়ে দিন।

পরের পর্বে: Account a = new CurrentAccount(...) — parent টাইপের ভ্যারিয়েবলে child object রাখলে কী হয়? a.withdraw() কোন সংস্করণ চালায়, আর JVM সেটা চলার সময় কীভাবে ঠিক করে — polymorphism আর dynamic dispatch, এই পর্বের method lookup-এর আসল শক্তি।

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

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