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

constructor আর this — জন্ম থেকেই বৈধ object

Java Constructors and this — Overloading, Chaining, Validation and Initialization Order

Taught in Bangla, this lesson explains constructors and this: why objects should be valid from birth, the order of object creation, when the default constructor disappears, resolving name clashes with this, constructor chaining with this(...), validation and final fields, copy constructors, static factories, builders, and why Java has no destructor.

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

  • Constructor কেন দরকার, তার নিয়ম, আর new চাপলে object তৈরির ধাপগুলো কোন ক্রমে চলে
  • Default constructor কখন থাকে আর কখন হারায় — javac-এর আসল error মেসেজসহ
  • this-এর তিন ব্যবহার: field-এর নাম আলাদা করা, this(...) দিয়ে constructor chaining, আর return this দিয়ে method chaining
  • Constructor-এ যাচাই, final field, copy constructor, static factory, builder আর record-এর এক ঝলক
  • Java-তে destructor কেন নেই, finalize কেন বাতিলের পথে, আর AI-এর লেখা constructor যাচাইয়ের উপায়

১ · Constructor — object-এর জন্মমুহূর্ত

পর্ব ১৫-এ আপনি class লিখে new দিয়ে object বানিয়েছেন, তারপর একটি একটি করে field-এ মান বসিয়েছেন। একটি দোকানের পণ্যের জন্য সেটা দেখতে এমন:

Product p = new Product();   // এই মুহূর্তে name = null, price = 0.0
p.name = "Chal 5kg";
p.price = 420.0;             // এর আগে কেউ p ব্যবহার করলে?

প্রথম আর তৃতীয় লাইনের মাঝখানে p একটি আধা-তৈরি object: নাম আছে, দাম নেই। কেউ ভুলে দামের লাইনটি না লিখলে প্রোগ্রাম চলবে, আর ক্যাশ মেমোতে ছাপা হবে “Chal 5kg — 0.0 টাকা”. কেউ p.price = -420 লিখলে সেটাও আটকানোর কেউ নেই। Object-এর invariant — যে শর্ত সবসময় সত্য থাকার কথা, যেমন “দাম কখনো ঋণাত্মক নয়” — রক্ষা করার কোনো নির্দিষ্ট জায়গা নেই।

সমাধান হলো একটি বিশেষ method, যা object জন্মানোর মুহূর্তে ঠিক একবার চলে, প্রয়োজনীয় সব মান নেয়, যাচাই করে, তারপর field-এ বসায়। এর নাম constructor. Constructor শেষ না হওয়া পর্যন্ত বাইরের কেউ object-টির reference পায় না — তাই বাইরের দুনিয়া কখনো আধা-তৈরি object দেখে না।

হাসপাতালে শিশু জন্মালে নাম, জন্মতারিখ আর ওজন লিখে একটি জন্মনিবন্ধন হয় — জন্মের সময়ই, পরে কোনো একদিন নয়। নিবন্ধন ছাড়া শিশুকে কেউ “অর্ধেক মানুষ” বলে না, কিন্তু সরকারি খাতায় তার অস্তিত্বই অসম্পূর্ণ। Constructor হলো object-এর জন্মনিবন্ধন: যে তথ্য ছাড়া object অর্থহীন, সেটা জন্মের মুহূর্তেই দিতে হবে, আর ভুল তথ্য (জন্মতারিখ আগামী বছর) হলে নিবন্ধনই হবে না।

Constructor-এর তিন নিয়ম

class Product {
    String name;
    double price;

    Product(String n, double p) {   // constructor: নাম = class-এর নাম, return type নেই
        name = n;
        price = p;
    }
}

Product p = new Product("Chal 5kg", 420.0);   // জন্মের সময়েই সব মান

নাম হুবহু class-এর নাম

Product class-এর constructor-এর নাম Product — বড় হাতের P সহ। product(...) লিখলে সেটা constructor নয়, return type ছাড়া একটি ভুল method, আর compiler error দেবে।

case-sensitive

কোনো return type নেই

void-ও নয়। Constructor কিছু “ফেরত দেয়” না; new নিজেই নতুন object-এর reference ফেরত দেয়।

void-ও নয়

শুধু new-এর সাথে চলে

p.Product("Dal", 150) বলে পরে আবার ডাকা যায় না। জন্ম একবারই হয় — নতুন মান চাইলে নতুন object, বা আলাদা method.

ঠিক একবার

Access modifier চলে

public, private ইত্যাদি বসানো যায় (পর্ব ১৭)। কিন্তু static, final বা abstract constructor হয় না।

modifier

যে void নীরবে সব নষ্ট করে

সবচেয়ে ধূর্ত ভুলটি দেখুন। অভ্যাসবশত কেউ constructor-এর সামনে void লিখে ফেলল:

class Product {
    String name;
    void Product(String n) {   // ভুল! এটা এখন একটি সাধারণ method
        name = n;
    }
}

Product p = new Product("Chal 5kg");
Main.java:8: error: constructor Product in class Product cannot be applied to given types;
        Product p = new Product("Chal 5kg");
                    ^
  required: no arguments
  found:    String
  reason: actual and formal argument lists differ in length
1 error

Java-তে method-এর নাম class-এর নামের মতো হওয়া বৈধ (খারাপ রীতি, কিন্তু বৈধ)। তাই void Product(String n) compiler-এর চোখে একটি method, যার নাম ঘটনাক্রমে Product. Class-এ তাহলে কোনো constructor লেখা নেই, আর compiler নিজে একটি argument-হীন constructor দিয়েছে। ফলে new Product("Chal 5kg") মেলে না। আর যদি কেউ new Product() লিখত, কোনো error-ই আসত না — name নীরবে null থেকে যেত। তাই নিয়ম মুখস্থ রাখুন: constructor-এর সামনে কোনো টাইপ নেই।

মূল ধারণা

Constructor-এর আসল কাজ field-এ মান বসানো নয় — সেটা তো যে-কেউ করতে পারে। আসল কাজ একটি প্রতিশ্রুতি: “এই class-এর যেকোনো object, যে মুহূর্তে আপনার হাতে আসবে, সে পুরোপুরি তৈরি আর বৈধ।” ভালো class-এ বাইরের কাউকে জন্মের পরে “বাকি কাজ” শেষ করতে হয় না।

২ · new চাপলে কী ঘটে — object তৈরির ক্রম

new Product("Chal", 420.0) — এক লাইন, কিন্তু ভেতরে কয়েকটি ধাপ নির্দিষ্ট ক্রমে চলে। এই ক্রম জানা থাকলে অনেক রহস্যময় bug সহজ হয়ে যায়। Constructor ছাড়াও field-এ মান বসানোর আরও দুটি জায়গা আছে: ঘোষণার সাথেই মান দেওয়া (int stock = 10;), আর class-এর ভেতরে একটি নামহীন { … } block, যাকে বলে instance initializer block. তিনটি কোন ক্রমে চলে, নিচে ধাপে ধাপে দেখুন:

1class Product {2    String name;               // initializer নেই3    int stock = 10;            // field initializer4    double price;5    { stock = stock + 5; }     // instance initializer block6    Product(String name, double price) {7        this.name = name;8        this.price = price;9    }10}11Product p = new Product("Chal", 420.0);
heap-এর নতুন object আর ভ্যারিয়েবল p
name
stock
price
p
  1. মেমরি, আর সব শূন্য। JVM heap-এ object-এর জন্য জায়গা নেয় আর প্রতিটি field-এ ডিফল্ট মান বসায়: সংখ্যায় 0 বা 0.0, boolean-এ false, reference-এ null. এজন্যই Java-তে field কখনো “আবর্জনা” মান ধরে না — C-র মতো নয়। p-তে এখনো কিছু বসেনি।
  2. Constructor ডাকা হলো। Argument "Chal" আর 420.0 কপি হয়ে parameter-এ বসল। Constructor-এর একদম শুরুতে একটি লুকানো super() চলে — parent class-এর (এখানে Object) constructor. এর বিস্তারিত পর্ব ১৯-এ। মনে রাখুন শুধু: parent আগে তৈরি হয়, তারপর আপনার class.
  3. Field initializer. super()-এর পরে, constructor-এর বাকি শরীরের আগে, ঘোষণার সাথে দেওয়া মানগুলো বসে। stock এখন 10.
  4. Instance initializer block. Field initializer আর { … } block — দুটি মিলে class-এ যে ক্রমে লেখা, সেই ক্রমে চলে। এখানে block নিচে লেখা, তাই পরে চলল: stock হলো 15. বাস্তবে initializer block খুব কম লেখা হয়; জানা দরকার যাতে অন্যের কোডে দেখে চমকে না যান।
  5. Constructor-এর শরীর। এবার আপনার লেখা লাইনগুলো: this.name = name আর this.price = price. Constructor শেষ হলে object পুরোপুরি তৈরি।
  6. Reference ফেরত। সবশেষে new expression নতুন object-এর reference ফেরত দেয়, আর সেটা বসে p-তে। এর আগে বাইরের কোনো কোড এই object দেখতে পায়নি — আধা-তৈরি object বাইরে না আসার নিশ্চয়তা এখান থেকেই।

একটি ছোট পরীক্ষা: ওপরের class-এ যদি constructor-এর শরীরে stock = 3; লেখা থাকত, শেষে stock কত হতো? উত্তর 3 — কারণ constructor-এর শরীর সবার শেষে চলে, আগের 10 আর 15-কে মুছে দিয়ে। নিয়মটি এক লাইনে: ডিফল্ট মান → (super) → field initializer ও block, লেখার ক্রমে → constructor-এর শরীর।

৩ · Default constructor — কখন আছে, কখন হারায়

পর্ব ১৫-এ আপনি কোনো constructor না লিখেই new Student() লিখেছিলেন, আর সেটা চলেছিল। কারণ: class-এ একটিও constructor না থাকলে compiler নিজে একটি argument-হীন, শরীর-খালি constructor যোগ করে দেয়। একে বলে default constructor. এর access modifier হয় class-এর মতোই — public class হলে সেটাও public. ভেতরে শুধু ওই লুকানো super().

কিন্তু যে মুহূর্তে আপনি নিজে যেকোনো একটি constructor লেখেন, compiler আর কিছু যোগ করে না। যুক্তিটা সহজ: আপনি একটি constructor লিখে জানিয়ে দিয়েছেন “এই object বানাতে এগুলো লাগবেই।” তখন compiler পেছনের দরজা খোলা রাখলে আপনার নিয়ম ভেঙে যেত।

class Ticket {
    String route;
    Ticket(String route) {          // একটি parameterized constructor
        this.route = route;
    }
}

Ticket t = new Ticket();             // আগে চলত, এখন?
Main.java:8: error: constructor Ticket in class Ticket cannot be applied to given types;
        Ticket t = new Ticket();
                   ^
  required: String
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error

মেসেজটি পড়তে শিখুন, কারণ এটি বারবার দেখবেন। required: String — যে constructor আছে, সে একটি String চায়। found: no arguments — আপনি কিছুই দেননি। একাধিক constructor থাকলে javac প্রতিটির তালিকা দিয়ে বলে no suitable constructor found for Ticket(no arguments). সমাধান দুটি: হয় সঠিক argument দিন, নয়তো সত্যিই argument ছাড়া object বানানো অর্থপূর্ণ হলে নিজে একটি argument-হীন constructor লিখুন। নিজে লেখা argument-হীন constructor-কে প্রায়ই no-arg constructor বলা হয়; “default constructor” নামটি কঠোরভাবে শুধু compiler-এর বানানোটির জন্য।

যে constructor parameter নেয়, তাকে বলে parameterized constructor. বেশিরভাগ বাস্তব class-এ এটিই প্রধান constructor, কারণ বেশিরভাগ object-এর অন্তত কিছু তথ্য ছাড়া কোনো অর্থ নেই — নামহীন পণ্য, রুটহীন টিকিট।

বাস্তব ফাঁদ: অনেক framework (যেমন JPA/Hibernate, পর্ব ৪৯–৫১-এর জগতে) এবং কিছু পুরোনো library object বানাতে argument-হীন constructor খোঁজে। আপনি parameterized constructor যোগ করলেন, আর হঠাৎ এমন জায়গায় error এলো যেখানে কোড বদলাননি — কারণ default constructor নীরবে হারিয়ে গেছে। তখন একটি no-arg constructor নিজে যোগ করতে হয় (অনেক সময় protected রেখে, যাতে সাধারণ কোড সেটা ব্যবহার না করে)।

৪ · this — “আমি যে object-এর ভেতরে আছি”

একটি class থেকে হাজারটা object তৈরি হতে পারে, কিন্তু method আর constructor-এর কোড একটিই। তাহলে name = … লিখলে কোন object-এর name বদলায়? উত্তর: যে object-এর ওপর এই মুহূর্তে কোডটি চলছে। প্রতিটি instance method আর constructor একটি লুকানো reference পায়, যা সেই object-কে দেখায়। Java-তে তার নাম this.

একটি গার্মেন্টস কারখানায় একশো জন অপারেটরের হাতে একই নির্দেশপত্র: “নিজের মেশিনের সুই বদলান।” কাগজ এক, কিন্তু প্রত্যেকে বোঝেন “নিজের” মানে তাঁর সামনের মেশিনটি। this হলো সেই “নিজের” শব্দ — কোড এক, কিন্তু প্রতিবার যে object-এর জন্য চলছে, this তাকেই দেখায়।

সবচেয়ে সাধারণ ব্যবহার — নামের সংঘাত মেটানো

অংশ ১-এ parameter-এর নাম ছিল n আর p — পড়তে অস্বস্তিকর। স্বাভাবিক ইচ্ছা হলো parameter-এর নামও name আর price রাখা। কিন্তু তখন constructor-এর ভেতরে name বললে দুটি জিনিস বোঝাতে পারে: field, অথবা parameter. Java-র নিয়ম: ভেতরের scope-এর নাম বাইরেরটাকে ঢেকে দেয়। Parameter ভেতরের, তাই সাধারণ name মানে parameter. একে বলে shadowing. Field-এ পৌঁছাতে হলে বলতে হয় this.name — “এই object-এর name”. নিচে দেখুন this ভুলে গেলে কী ঘটে:

ভুল সংস্করণnew Customer("Rahim") — constructor শুরু
Customer(String name) {
    name = name;        // কোন name কোনটা?
}

constructor-এর frame        heap-এর object
┌──────────────────┐        ┌─────────────────────┐
│ name = "Rahim"   │        │ this.name = null    │
└──────────────────┘        └─────────────────────┘
দুটি আলাদা name — parameter আর field
ভুল সংস্করণname = name; চলার পরে
constructor-এর frame        heap-এর object
┌──────────────────┐        ┌─────────────────────┐
│ name = "Rahim"   │◀─┐     │ this.name = null    │
└──────────────────┘  │     └─────────────────────┘
           └──নিজেকে নিজে─┘
দুই পাশের name-ই parameter; field-এ কেউ হাত দেয়নি
ভুল সংস্করণconstructor শেষ
constructor-এর frame        heap-এর object
   (মুছে গেছে)              ┌─────────────────────┐
                            │ this.name = null    │
                            └─────────────────────┘
c.greet()  →  Hello, null
compile error নেই, crash নেই — শুধু ভুল ফল
সঠিক সংস্করণthis.name = name; চলার পরে
Customer(String name) {
    this.name = name;   // বাঁয়ে field, ডানে parameter
}

constructor-এর frame        heap-এর object
┌──────────────────┐ কপি   ┌─────────────────────┐
│ name = "Rahim"   │──────▶│ this.name = "Rahim" │
└──────────────────┘        └─────────────────────┘
c.greet()  →  Hello, Rahim
  1. দুটি name. Constructor-এর ভেতরে একটি parameter name (মান "Rahim") আর object-এর একটি field name (মান এখনো null) একসাথে আছে। সাধারণভাবে name লিখলে compiler বেছে নেয় সবচেয়ে কাছের, ভেতরের scope-এর নামটি — parameter.
  2. নিজেকে নিজে assign. name = name; মানে “parameter-এ parameter-এর মান বসাও।” কাজটি সম্পূর্ণ অর্থহীন, কিন্তু Java-র নিয়মে পুরোপুরি বৈধ। javac কোনো error দেয় না; IntelliJ-এর মতো IDE অবশ্য হলুদ দাগ দিয়ে সতর্ক করে।
  3. নীরব bug. Constructor শেষ, parameter-এর frame মুছে গেল, আর "Rahim" চিরতরে হারাল। Field এখনো null. পরে কোথাও “Hello, null” ছাপা হলো, বা আরও খারাপ — name.length() ডাকতেই NullPointerException. Bug-টি যেখানে দেখা দেয়, তার জন্ম অনেক দূরে।
  4. সমাধান — this. this.name মানে নির্দিষ্টভাবে “এই object-এর field”. বাঁ পাশে field, ডান পাশে parameter — মান ঠিক জায়গায় পৌঁছাল। Java দুনিয়ায় এটাই প্রচলিত রীতি: parameter-এর নাম field-এর নামে রেখে this. দিয়ে আলাদা করা। Field-এ final দিলে ভুলটি compile error-এ ধরা পড়ে (অংশ ৬)।

this নিয়ে আরও তিনটি তথ্য। (১) যেখানে নামের সংঘাত নেই, সেখানে this. ঐচ্ছিক — price * qty আর this.price * this.qty একই জিনিস। (২) static method-এ কোনো object নেই, তাই this-ও নেই; main-এর ভেতরে this লিখলে javac বলে non-static variable this cannot be referenced from a static context (পর্ব ১৮)। (৩) this একটি সাধারণ reference — তাকে অন্য কোথাও পাঠানো বা ফেরত দেওয়া যায়, যা অংশ ৭-এ দেখবেন।

৫ · Constructor overloading আর this(...) chaining

বাস কাউন্টারে টিকিট কাটার কয়েকটি উপায় আছে। কেউ শুধু বলেন “একটা টিকিট দেন” — তখন ডিফল্ট রুট, যেকোনো সিট। কেউ বলেন “খুলনার টিকিট” — রুট আছে, সিট যেকোনো। কেউ বলেন “খুলনা, সিট বারো”। পর্ব ১২-এর method overloading-এর মতোই একটি class-এ একাধিক constructor রাখা যায়, যদি তাদের parameter-তালিকা আলাদা হয়। একে বলে constructor overloading.

কিন্তু তিনটি constructor-এ যদি তিনবার একই যাচাই আর একই assignment লেখেন, তাহলে কোনো নিয়ম বদলালে তিন জায়গায় বদলাতে হবে। সমাধান: একটি constructor-কে “আসল” বানান, যেটি সব কাজ করে; বাকিরা ডিফল্ট মান জুড়ে তাকে কাজ দিয়ে দেয়। একই class-এর আরেক constructor ডাকার উপায় হলো this(...), আর এই কৌশলের নাম constructor chaining. চালিয়ে দেখুন কোন ক্রমে লাইন ছাপা হয়:

class Ticket {
    final String route;
    final int seat;                          // 0 মানে যেকোনো সিট
    Ticket() {
        this("Dhaka-Sylhet");                // প্রথম statement হতেই হবে
        System.out.println("3: Ticket()");
    }
    Ticket(String route) {
        this(route, 0);
        System.out.println("2: Ticket(route)");
    }
    Ticket(String route, int seat) {
        System.out.println("1: Ticket(route, seat)");
        this.route = route;  this.seat = seat;
    }
}
class Main {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        System.out.println(t.route + ", seat " + t.seat);
    }
}

আউটপুট চার লাইন: 1: Ticket(route, seat), 2: Ticket(route), 3: Ticket(), Dhaka-Sylhet, seat 0. ডেকেছিলেন argument-হীন Ticket(), অথচ তার লাইন ছাপা হলো সবার শেষে! কারণ প্রতিটি constructor প্রথমেই this(...) দিয়ে পরেরটিকে কাজ দেয়, আর নিজের বাকি লাইনগুলো চলে সেই কাজ শেষ হওয়ার পরে — ঠিক পর্ব ১২-এর call stack-এর মতো। Field দুটি final, আর শুধু তিন-parameter constructor তাদের মান দেয়; বাকি দুটি this(...) দিয়ে তাকেই ডাকে বলে compiler সন্তুষ্ট। এখানে Ticket আর Main দুটিই public ছাড়া, কারণ অনলাইন runner ফাইলটিকে অন্য নামে compile করে; নিজের প্রজেক্টে সাধারণত প্রতিটি public class নিজের নামের ফাইলে থাকে।

ধাপে ধাপে দেখুন constructor-গুলো কীভাবে একটির ভেতরে আরেকটি খোলে, আর উল্টো ক্রমে শেষ হয়:

1Ticket() {2    this("Dhaka-Sylhet");3    System.out.println("3: Ticket()");4}5Ticket(String route) {6    this(route, 0);7    System.out.println("2: Ticket(route)");8}9Ticket(String route, int seat) {10    System.out.println("1: Ticket(route, seat)");11    this.route = route;12    this.seat = seat;13}14Ticket t = new Ticket();
object-এর অবস্থা
সক্রিয় constructor
this.route
this.seat
t
  1. new Ticket() — argument-হীন constructor মিলল। কিন্তু তার প্রথম লাইনই this("Dhaka-Sylhet"). নিজের কাজ শুরুর আগেই সে এক-parameter সংস্করণকে ডাকছে। Ticket() এখানে থেমে থাকল।
  2. Ticket(String) খুলল, route = "Dhaka-Sylhet". এরও প্রথম লাইন this(route, 0) — ডিফল্ট সিট 0 জুড়ে আরও এক ধাপ নিচে। এখন দুটি constructor অপেক্ষায়।
  3. আসল constructor চলছে। Ticket(String, int)-এর কোনো this(...) নেই, তাই সে-ই প্রথম নিজের শরীর চালায় — ছাপা হলো 1: …. (এর শুরুতে লুকানো super() আছে; শিকলে শুধু এই শেষ constructor-টিই super() ডাকে, আর field initializer-ও শুধু এখানেই চলে — একবারই।)
  4. Field-এ মান বসল। this.route আর this.seat দেওয়া হলো। দুটি final field ঠিক একবার মান পেল — compiler ঠিক এটাই যাচাই করে।
  5. ফিরে এলাম Ticket(String)-এ। this(route, 0)-এর পরের লাইন চলল — ছাপা হলো 2: ….
  6. সবশেষে Ticket(). ছাপা হলো 3: …, আর new পূর্ণ object-এর reference দিল t-কে। যে constructor সবার আগে ডাকা হয়েছিল, সে সবার শেষে শেষ হলো। সব যাচাই আর assignment একটিমাত্র জায়গায় — নিয়ম বদলাতে এক জায়গায় হাত দিলেই চলবে।

this(...)-এর নিয়ম

  • প্রথম statement হতেই হবে। this(...)-এর আগে একটি println লিখলে Java 21-এ javac বলে call to this must be first statement in constructor. যুক্তি: অন্য constructor object-টিকে প্রস্তুত করার আগে এই constructor তার ওপর কাজ শুরু করলে আধা-তৈরি object ব্যবহারের ঝুঁকি। (পরের সংস্করণে এই নিয়ম কিছুটা শিথিল হয়েছে: Java 22–24-এ preview হিসেবে, আর Java 25-এ JEP 513-এর মাধ্যমে স্থায়ীভাবে। এখন যে statement তৈরি-হতে-থাকা object-টি ব্যবহার করে না — যেমন argument যাচাই — সেটা this(...) বা super(...)-এর আগে লেখা যায়। এই কোর্স Java 21-এ, তাই আপাতত পুরোনো নিয়মই মানুন।)
  • একটি constructor-এ বড়জোর একটি this(...) — আর this(...) ও super(...) একসাথে নয়, কারণ দুটিই প্রথম হতে চায়।
  • বৃত্ত চলবে না। A যদি B-কে আর B যদি A-কে ডাকে, javac বলে recursive constructor invocation — compile-এর সময়েই ধরা পড়ে।
  • সাধারণত ছোট থেকে বড় দিকে ডাকুন: কম parameter-এর constructor ডিফল্ট মান জুড়ে বেশি parameter-এর constructor-কে ডাকে, আর সবচেয়ে বড়টি আসল কাজ করে।

৬ · যাচাই — অবৈধ object জন্মাতেই দেবেন না

এবার constructor-এর আসল শক্তি। পণ্যের দাম ঋণাত্মক হতে পারে না, নাম ফাঁকা হতে পারে না, স্টক শূন্যের কম হতে পারে না। এই শর্তগুলো constructor-এ যাচাই করলে, আর ভুল হলে object তৈরিই বন্ধ করে দিলে, পুরো প্রোগ্রামে কোথাও একটি অবৈধ Product থাকার সম্ভাবনাই থাকে না। বন্ধ করার উপায় হলো একটি exception ছোড়া — Java-তে ভুল argument-এর জন্য প্রচলিত exception হলো IllegalArgumentException:

class Product {
    private final String name;
    private final double price;
    private final int stock;
    Product(String name, double price, int stock) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name is empty");
        if (price < 0) throw new IllegalArgumentException("price < 0: " + price);
        if (stock < 0) throw new IllegalArgumentException("stock < 0: " + stock);
        this.name = name;
        this.price = price;
        this.stock = stock;
    }
    @Override public String toString() { return name + " | " + price + " tk | stock " + stock; }
}
class Main {
    public static void main(String[] args) {
        System.out.println(new Product("Chal 5kg", 420.0, 30));
        try { new Product("Dal 1kg", -150, 10); }
        catch (IllegalArgumentException e) { System.out.println("Rejected: " + e.getMessage()); }
    }
}

আউটপুট: Chal 5kg | 420.0 tk | stock 30 আর Rejected: price < 0: -150.0. প্রথম পণ্য সব যাচাই পার হয়ে জন্মাল। দ্বিতীয়টিতে দাম −150, তাই দ্বিতীয় if-এ exception ছোড়া হলো, constructor সেখানেই থেমে গেল, আর new কোনো reference ফেরতই দিল না — সেই অবৈধ object কোনো ভ্যারিয়েবলে কখনো পৌঁছায়নি। -150 ছিল int, double parameter-এ widening হয়ে -150.0 হয়েছে। try/catch আর exception-এর পূর্ণ আলোচনা পর্ব ২৯-এ; toString override করার নিয়ম পর্ব ২৩-এ। আপাতত শুধু ধরুন: throw মানে “এখানেই থামো, ভুলটা ওপরে জানাও”।

এই কৌশলের নাম fail-fast: ভুল যত আগে, যত কাছে ধরা পড়ে, খুঁজে পাওয়া তত সহজ। দাম −150 যদি চুপচাপ ঢুকে যেত, ভুলটা ধরা পড়ত হয়তো মাসের শেষে হিসাব না মেলায় — তখন কোন লাইন থেকে এলো, খুঁজে পাওয়া কঠিন। Exception message-এ খারাপ মানটিও রাখুন ("price < 0: " + price) — পরে log পড়তে সুবিধা হয়।

final field — constructor-কে বাধ্য করা

ওপরের field তিনটি final. final field-এ ঠিক একবার মান দিতে হয় — ঘোষণার সাথে, অথবা প্রতিটি constructor-এর প্রতিটি পথে। কোনো constructor কোনো final field-এ মান দিতে ভুলে গেলে javac সেই constructor-এর শেষে বলে variable price might not have been initialized. তাই অংশ ৪-এর name = name ভুলটি final field-এ হলে compile-এর সময়েই ধরা পড়ত — নীরব bug হয়ে থাকত না। আর জন্মের পরে কেউ p.price = -5 লিখতে চাইলে সেটাও compile error. final-এর পুরো গল্প পর্ব ১৮-এ।

Constructor-এর পরে setter দিয়ে মান বসানো — একটি ফাঁদ। অনেক কোড (আর অনেক AI) এভাবে লেখে: Product p = new Product(); p.setName("Chal"); p.setPrice(420);. এতে আবার অংশ ১-এর সমস্যা ফিরে আসে — দুই setter-এর মাঝখানে object অর্ধেক তৈরি, আর একটি setter ডাকা ভুলে গেলে object চিরকাল অসম্পূর্ণ। যা ছাড়া object-এর অর্থ নেই, তা constructor-এ নিন। Setter থাকলে সেখানেও একই যাচাই রাখুন — encapsulation আর setter-এ যাচাইয়ের বিস্তারিত পরের পর্বে।

৭ · this পাঠানো আর this ফেরত দেওয়া

নিজেকে অন্য method-এ পাঠানো

this যেহেতু একটি সাধারণ reference, object নিজেকে argument হিসেবে অন্য কারও কাছে পাঠাতে পারে। যেমন টিকিট নিজেই কাউন্টারের খাতায় নিজের নাম তোলে:

class Ticket {
    void book(Counter counter) {
        counter.register(this);   // “আমাকে খাতায় তোলো”
    }
}

এটা পুরোপুরি স্বাভাবিক — event listener, observer আর parent-child সম্পর্কে (যেমন একটি Order তার প্রতিটি OrderItem-কে নিজের reference দিয়ে রাখে) এর ব্যবহার সবখানে। শুধু একটি সতর্কতা: constructor-এর ভেতর থেকে this বাইরে পাঠাবেন না। তখনো constructor শেষ হয়নি, কিছু field হয়তো এখনো মান পায়নি — অথচ বাইরের কেউ object-টির reference পেয়ে গেল। অংশ ২-এর “আধা-তৈরি object বাইরে আসে না” প্রতিশ্রুতি আপনি নিজেই ভাঙলেন। একাধিক thread থাকলে (পর্ব ৪০) এটা বিশেষ বিপজ্জনক।

নিজেকে ফেরত দেওয়া — method chaining

পিৎজা অর্ডারের কথা ভাবুন: সাইজ বাধ্যতামূলক, টপিং যত খুশি, চিজ ঐচ্ছিক। প্রতিটি বিকল্পের জন্য constructor বানালে অসংখ্য constructor লাগবে। তার বদলে সাইজ constructor-এ নিন, আর বাকি প্রতিটি method কাজ শেষে return this; করুক — অর্থাৎ নিজেকেই ফেরত দিক:

class Pizza {
    private final String size;
    private final List<String> toppings = new ArrayList<>();
    private boolean cheese;

    Pizza(String size) { this.size = size; }

    Pizza addTopping(String t) { toppings.add(t); return this; }
    Pizza addCheese()          { cheese = true;   return this; }

    int total() {
        int base = size.equals("Large") ? 800 : 600;
        return base + toppings.size() * 80 + (cheese ? 100 : 0);
    }
}

এখন প্রতিটি ডাকের ফল আবার একই Pizza, তাই তার ওপর সরাসরি পরের method ডাকা যায় — এটাই method chaining. যখন পুরো API এভাবে বাক্যের মতো পড়ার জন্য নকশা করা হয়, তাকে বলে fluent interface. ধাপে ধাপে দেখুন প্রতিটি ডাক কী ফেরত দেয়:

1Pizza p = new Pizza("Medium")2        .addTopping("Chicken")3        .addTopping("Olive")4        .addCheese();5System.out.println(p.total());
একমাত্র Pizza object-এর ভেতরে
size
toppings
cheese
  1. Constructor — বাধ্যতামূলক অংশ। new Pizza("Medium") একটি object বানাল (এখানে তাকে “Pizza #1” বলছি), টপিং-তালিকা খালি, চিজ নেই। লাইন শেষ হয়নি — সেমিকোলন অনেক নিচে, তাই এই reference-এর ওপরেই পরের ডাক।
  2. addTopping("Chicken"). তালিকায় Chicken যোগ হলো, তারপর return this — অর্থাৎ নতুন কিছু নয়, সেই একই Pizza #1-এর reference ফেরত গেল।
  3. আবার addTopping. আগের ডাকের ফল ছিল Pizza #1, তাই Olive-ও যোগ হলো সেই একই object-এ। কোনো কপি তৈরি হচ্ছে না।
  4. addCheese(), তারপর সেমিকোলন। চিজ যোগ হলো, শেষ return this-এর মান বসল p-তে — যা প্রথম থেকেই একই object.
  5. ফল। total() = Medium-এর 600 + দুটি টপিং × 80 + চিজ 100 = 860. চারটি ডাক, একটি object, আর কোডটি প্রায় অর্ডার স্লিপের মতো পড়া যায়। StringBuilder-এর sb.append("a").append("b") ঠিক এভাবেই কাজ করে — প্রতিটি append নিজেকে ফেরত দেয়।

একটি সূক্ষ্ম কথা: এখানে প্রতিটি method একই object বদলায় (mutable). String-এর মতো immutable class-এ chaining দেখতে একই রকম — s.trim().toUpperCase() — কিন্তু সেখানে প্রতিটি ধাপ একটি নতুন object ফেরত দেয়, this নয়। আর Pizza-র আরেকটি দুর্বলতা আছে: size-এর মতো বাধ্যতামূলক মান অনেকগুলো হলে constructor আবার লম্বা হয়ে যায়। তার সমাধান builder — পরের অংশে।

৮ · কপি, factory, builder আর record

সাধারণ constructor সব প্রয়োজন মেটায় না। পেশাদার Java কোডে object তৈরির আরও চারটি ধরন বারবার দেখবেন — এখানে এক ঝলক, যাতে চিনতে পারেন।

Copy constructor — clone নয়

একটি পণ্যের হুবহু আরেকটি কপি চাই। পর্ব ১৩ আর ১৫-এ দেখেছেন, Product b = a; কপি বানায় না — দুটি ভ্যারিয়েবল একই object দেখায় (aliasing). Java-তে প্রচলিত উপায় হলো copy constructor — একটি constructor যে একই class-এর আরেকটি object নেয়:

Product(Product other) {
    this(other.name, other.price, other.stock);   // যাচাই-সহ আসল constructor-কে কাজ দিলাম
}

Product copy = new Product(original);

Java-র Object class-এ একটি clone() method আছে, আর নতুনরা প্রায়ই সেটাই খোঁজেন। কিন্তু অভিজ্ঞ Java প্রোগ্রামাররা সাধারণত এড়িয়ে চলেন: clone ব্যবহার করতে Cloneable নামের একটি অদ্ভুত interface লাগে যাতে কোনো method নেই, clone নিজে protected, ফেরত দেয় Object (cast লাগে), কোনো constructor না ডেকেই object বানায় — ফলে আপনার যাচাই এড়িয়ে যায় — আর ডিফল্টভাবে shallow copy করে। Joshua Bloch-এর Effective Java বইয়ে (Item 13) clone-এর সমস্যাগুলো বিস্তারিত বলা আছে, আর সুপারিশ হলো copy constructor বা copy factory. লক্ষ রাখুন, copy constructor-ও নিজে থেকে deep copy করে না: field-এ যদি একটি List থাকে, নতুন list বানিয়ে কপি করতে হবে, নইলে দুটি object একই list ভাগ করবে।

Static factory method — নাম-ওয়ালা constructor

আপনি ইতিমধ্যে অনেক ব্যবহার করেছেন: List.of(1, 2, 3), Integer.valueOf("42"), String.valueOf(3.5), পরে দেখবেন LocalDate.of(2026, 10, 18). এগুলো constructor নয় — static method, যা ভেতরে object বানিয়ে (বা আগের একটি ফেরত দিয়ে) দেয়। একে বলে static factory method. সুবিধা: নাম থাকে (Temperature.ofCelsius(30) বনাম Temperature.ofFahrenheit(86) — দুটিই একটি double নেয়, constructor দিয়ে আলাদা করা অসম্ভব), প্রতিবার নতুন object বানাতে হয় না (Integer.valueOf −128 থেকে 127 পর্যন্ত আগের object ফেরত দেয়), আর ফেরত দেওয়া object-এর আসল class লুকিয়ে রাখা যায়। প্রচলিত নাম: of, valueOf, from, getInstance, newInstance. প্রায়ই constructor-কে private রেখে শুধু factory খোলা রাখা হয় (পর্ব ১৭, ১৮)।

Builder — অনেক parameter হলে

একটি বাস রিজার্ভেশনে রুট, তারিখ, সময়, সিট, যাত্রীর নাম, ফোন, এসি না নন-এসি, খাবার, ছাড়… দশটি parameter. Constructor-এ new Booking("Dhaka-Khulna", d, t, 12, "Rina", "017…", true, false, 0) — কোন true কোনটা, কেউ মনে রাখতে পারে না। প্রতিটি ঐচ্ছিক সমন্বয়ের জন্য আলাদা constructor বানালে সেগুলো ক্রমশ লম্বা হতে থাকে — একে বলে telescoping constructor. সমাধান builder pattern:

Booking b = Booking.builder()
        .route("Dhaka-Khulna")
        .seat(12)
        .passenger("Rina")
        .ac(true)
        .build();          // এখানে সব যাচাই, তারপর একবারে object

খেয়াল করুন, এটা অংশ ৭-এর method chaining-ই, কিন্তু একটি পার্থক্যসহ: chaining হয় একটি আলাদা builder object-এর ওপর, আর আসল Booking জন্মায় শুধু build()-এ, সব যাচাই পার হওয়ার পরে। তাই আসল object-কে immutable রাখা যায়, আর কখনো আধা-তৈরি অবস্থায় দেখা যায় না। পুরো নকশা পর্ব ৪৮-এ।

Record — constructor নিজেই তৈরি হয়

যে class শুধু কয়েকটি মান বহন করে, তার জন্য Java 16 থেকে record আছে। record Point(int x, int y) {} লিখলেই compiler দুটি private final field, সব field নেওয়া একটি constructor (একে বলে canonical constructor), x() আর y() accessor, আর equals, hashCode, toString বানিয়ে দেয়। যাচাই চাইলে একটি ছোট “compact” constructor লেখা যায়:

record Price(double amount) {
    Price {                                   // compact constructor: parameter-তালিকা নেই
        if (amount < 0) throw new IllegalArgumentException("amount < 0");
    }                                         // field-এ assignment compiler নিজে করে
}

Record-এর পুরো আলোচনা পর্ব ২৫-এ। এখানে শুধু লক্ষ করুন, ভাষার নকশাকারেরাও একই সিদ্ধান্তে পৌঁছেছেন: সব মান constructor-এ, যাচাই constructor-এ, তারপর আর বদল নয়।

৯ · Java-তে destructor নেই — তাহলে পরিষ্কার করে কে?

C++-এ প্রতিটি constructor-এর একটি জোড়া আছে — destructor (~Product()), যা object-এর জীবন শেষে নিশ্চিতভাবে চলে আর মেমরি, ফাইল, lock সব ফেরত দেয়। Java-তে এমন কিছু নেই। কারণ মেমরির দায়িত্ব প্রোগ্রামারের নয়, JVM-এর: যখন কোনো object-এর দিকে আর কোনো reference থাকে না, garbage collector একসময় তার জায়গা ফেরত নেয় (পর্ব ৫, বিস্তারিত পর্ব ৪৪)। কখন নেবে, তা নির্দিষ্ট নয় — এক সেকেন্ড পরে, এক ঘণ্টা পরে, বা প্রোগ্রাম শেষ হওয়া পর্যন্ত কখনোই না।

মেমরির জন্য এটা চমৎকার। কিন্তু খোলা ফাইল, ডেটাবেস connection বা network socket অপারেটিং সিস্টেমের সীমিত সম্পদ — সেগুলো “একসময়” নয়, কাজ শেষ হওয়া মাত্র বন্ধ করা দরকার। Java-র উত্তর হলো try-with-resources (Java 7 থেকে):

try (var reader = Files.newBufferedReader(Path.of("bill.txt"))) {
    System.out.println(reader.readLine());
}   // এখানে reader.close() নিশ্চিতভাবে চলে — exception হলেও

এটা পর্ব ২৯ আর ৩৭-এ বিস্তারিত দেখবেন। তার আগে একটি পুরোনো জিনিস চিনে রাখুন: finalize(). Java 1.0 থেকে Object-এ এই method ছিল, যা GC object সরানোর আগে ডাকতে পারত। অনেকে একে destructor ভেবে ব্যবহার করতেন — কিন্তু কখন চলবে বা আদৌ চলবে কি না, তার কোনো নিশ্চয়তা ছিল না। Java 9-এ এটি deprecated হয়, আর Java 18-এ JEP 421 একে “deprecated for removal” ঘোষণা করে — অর্থাৎ ভবিষ্যতের কোনো সংস্করণে সরিয়ে ফেলা হবে। নতুন কোডে কখনো finalize লিখবেন না।

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

C++-এর মতো Java-তেও constructor-এর নাম class-এর নামে, overloading আর delegating (this(...)) আছে; কিন্তু Java-তে member initializer list নেই, destructor নেই, আর this pointer নয়, reference — তাই this->name নয়, this.name. Python-এর __init__ আসলে object তৈরির পরে চলা initializer, আর সেখানে self স্পষ্টভাবে লিখতে হয়; Java-তে this লুকানো থাকে, আর Python-এ overloading না থাকায় default argument ব্যবহার হয়, Java-তে overloading আর this(...). গভীরে তুলনা করতে দেখুন C++-এর constructor ও destructor আর Python-এর class ও object পর্ব।

পরীক্ষা ও ইন্টারভিউতে
  • Constructor আর method-এর পার্থক্য? — Constructor-এর নাম class-এর নামে, return type নেই, শুধু new-এর সাথে একবার চলে, আর inherit হয় না; method-এর return type থাকে আর যতবার খুশি ডাকা যায়।
  • Default constructor কখন পাওয়া যায়? — শুধু তখন, যখন class-এ একটিও constructor লেখা নেই; একটি লিখলেই compiler আর দেয় না।
  • this আর this()-এর পার্থক্য? — this হলো বর্তমান object-এর reference; this(...) একই class-এর আরেক constructor ডাকে আর সেটা constructor-এর প্রথম statement হতে হয়।
  • Constructor কি private হতে পারে? — হ্যাঁ; তখন বাইরে থেকে new চলে না, object দেওয়া হয় static factory দিয়ে (যেমন singleton বা utility class).
  • Java-তে destructor আছে? — না; মেমরি garbage collector ফেরত নেয়, অন্য সম্পদ বন্ধ করতে try-with-resources; finalize deprecated.

১০ · AI-এর যুগে — constructor-এর প্রতিশ্রুতি যাচাই করুন

AI-কে “একটি Product class লিখে দাও” বললে সে প্রায় সবসময় compile হয় এমন কোড দেয়। সমস্যা compile-এ নয়, প্রতিশ্রুতিতে: AI-এর লেখা constructor প্রায়ই শুধু field-এ মান কপি করে, যাচাই করে না — অর্থাৎ সে object-কে বৈধ রাখার দায়িত্বই নেয় না। IDE-র “Generate constructor” বোতামও ঠিক একই জিনিস বানায়। নিচের ভুলগুলো সবচেয়ে বেশি দেখা যায়:

AI যা করেকেন সমস্যাআপনি যা করবেন
Constructor-এ কোনো যাচাই নেইঋণাত্মক দাম, ফাঁকা নাম নিয়ে object জন্মায়, ভুল ধরা পড়ে অনেক পরেপ্রতিটি parameter-এর বৈধ সীমা লিখে দিন, IllegalArgumentException চান
খালি constructor + অনেক setterObject কিছু সময় আধা-তৈরি থাকে; একটি setter ভুলে গেলে চিরকাল অসম্পূর্ণবাধ্যতামূলক মান constructor-এ, field final
পাঁচ-ছয়টি প্রায় একই constructor (telescoping), প্রতিটিতে কপি করা কোডএকটিতে নিয়ম বদলালে বাকিগুলো পুরোনো থেকে যায়একটি আসল constructor, বাকিগুলো this(...); অনেক ঐচ্ছিক মান হলে builder
Parameter-এর নাম আলাদা রেখে বা this ভুলে name = nameনীরবে field null বা 0 থেকে যায়this. দেখে নিন; field final হলে compiler-ই ধরবে
কপির জন্য clone() বা constructor-এর ভেতর থেকে this বাইরে পাঠানোযাচাই এড়ায়, shallow copy; আধা-তৈরি object ফাঁসCopy constructor বা factory চান; constructor-এ শুধু নিজের field সাজানো

AI-কে class লিখতে বা রিভিউ করতে বলার সময় নিচের কাঠামো ব্যবহার করুন। শর্তগুলো না দিলে AI নিজের অভ্যাসে “getter-setter-সহ খালি class” দেবে:

আমি Java 21 শিখছি। নিচের জিনিসটির জন্য একটি class লিখে দাও। জিনিস: [যেমন — দোকানের পণ্য: নাম, দাম, স্টক] প্রতিটি field-এর বৈধ সীমা: [যেমন — নাম ফাঁকা নয়, দাম ০ বা বেশি, স্টক ০ বা বেশি] শর্ত: ১. বাধ্যতামূলক সব মান constructor-এ নাও; যে field পরে বদলাবে না, সেটা private final. ২. constructor-এ প্রতিটি সীমা যাচাই করো, ভুল হলে খারাপ মানসহ message দিয়ে IllegalArgumentException ছোড়ো। ৩. field আর parameter-এর নাম এক রাখো, this. দিয়ে আলাদা করো। ৪. একাধিক constructor লাগলে একটিতে সব কাজ রাখো, বাকিগুলো this(...) দিয়ে সেটাকে ডাকুক। ৫. খালি constructor + setter দিয়ে object বানানোর পথ রেখো না; ঐচ্ছিক মান অনেক হলে builder-এর কথা বলো। ৬. clone() ব্যবহার করো না; কপি লাগলে copy constructor দাও। ৭. শেষে main-এ একটি বৈধ আর একটি অবৈধ object বানানোর চেষ্টা দেখাও।

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

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

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

প্র ০১C++-এ destructor একটি মূল হাতিয়ার। Java কেন destructor রাখেনি, আর finalize() কেন শেষ পর্যন্ত বাতিলের পথে গেল?

C++-এর মডেল। C++-এ একটি local object scope শেষ হলেই তার destructor চলে — নিশ্চিতভাবে, নির্দিষ্ট মুহূর্তে। এর ওপর দাঁড়িয়ে আছে RAII (Resource Acquisition Is Initialization): constructor-এ সম্পদ নাও, destructor-এ ফেরত দাও। ফাইল, lock, মেমরি — সব একই নিয়মে। কিন্তু এর দাম হলো প্রতিটি object-এর মালিক কে আর কখন সে মরবে, তা প্রোগ্রামারকে ভাবতে হয়; ভুল হলে memory leak, dangling pointer, দুবার delete.

Java-র সিদ্ধান্ত। ১৯৯০-এর দশকে Java-র নকশাকারেরা মেমরি-সংক্রান্ত এই পুরো শ্রেণির bug দূর করতে চেয়েছিলেন, তাই বেছে নিলেন garbage collection: object-এর জীবন scope-এর সাথে বাঁধা নয়, reference যতক্ষণ আছে ততক্ষণ সে বাঁচে, আর GC নিজের সুবিধামতো সময়ে মৃতদের সরায়। একটি object অনেক জায়গা থেকে reference-এ ভাগ হতে পারে, তাই “ঠিক এই মুহূর্তে মরল” — এমন কোনো নির্দিষ্ট মুহূর্তই নেই। নির্দিষ্ট মুহূর্ত না থাকলে নিশ্চিত destructor-ও সম্ভব নয়।

finalize — একটি আপস যা কাজ করেনি। Java 1.0 তবু একটি হুক রেখেছিল: GC object সরানোর আগে finalize() ডাকতে পারে। সমস্যাগুলো বছরের পর বছর জমেছে। কখন চলবে তার নিশ্চয়তা নেই — ফাইল হয়তো কয়েক মিনিট খোলা থাকে, আর সীমা পেরোলে নতুন ফাইল খোলাই যায় না। আদৌ চলবে তারও নিশ্চয়তা নেই। Finalizer থাকা object সরাতে GC-কে বাড়তি ধাপ নিতে হয়, ফলে গতি কমে। Finalizer-এর ভেতরে this কোথাও রেখে দিলে মরতে বসা object “পুনর্জীবিত” হয়। ভেতরের exception নীরবে হারিয়ে যায়। আর আক্রমণকারী subclass-এর finalizer দিয়ে অর্ধেক-যাচাই হওয়া object হাতিয়ে নেওয়ার কৌশলও (finalizer attack) জানা ছিল।

বিকল্প আর বিদায়। Java 7 (২০১১) আনল try-with-resources আর AutoCloseable — ফাইল-সম্পদের জন্য নিশ্চিত, নির্দিষ্ট মুহূর্তে বন্ধ হওয়া, অনেকটা RAII-এর মতো, কিন্তু স্পষ্টভাবে লেখা। Java 9-এ finalize deprecated হলো আর একটি নিরাপদ বিকল্প java.lang.ref.Cleaner এলো, শেষ নিরাপত্তা-জাল হিসেবে। Java 18-এ JEP 421 finalization-কে “deprecated for removal” করে, আর পরীক্ষার জন্য --finalization=disabled option দেয়, যাতে প্রোগ্রামাররা দেখতে পারেন finalization ছাড়া তাঁদের প্রোগ্রাম চলে কি না। Java 21-এও method-টি আছে, কিন্তু ভবিষ্যতে সরানোর ঘোষণাসহ।

মূল উপলব্ধি: মেমরি আর অন্য সম্পদ দুটি আলাদা সমস্যা। Java মেমরি দিয়েছে GC-কে, আর বাকি সম্পদের জন্য চেয়েছে স্পষ্ট, নির্দিষ্ট মুহূর্তের বন্ধ করা — “কোনো একসময় হয়তো” ধরনের destructor দুই দিকেই খারাপ ছিল।

প্র ০২Joshua Bloch-এর Effective Java-র প্রথম Item-ই বলে “constructor-এর বদলে static factory method বিবেচনা করুন”। কেন এত গুরুত্ব? এর অসুবিধা কী?

Joshua Bloch Sun Microsystems-এ Java-র Collections Framework-সহ অনেক মূল library-র নকশা করেছিলেন। তাঁর Effective Java (প্রথম সংস্করণ ২০০১, তৃতীয় ২০১৮) Java প্রোগ্রামারদের মধ্যে প্রায় অবশ্যপাঠ্য। তিনটি সংস্করণেই প্রথম Item একই: Consider static factory methods instead of constructors. বইয়ের একদম শুরুতে object তৈরির প্রসঙ্গ — কারণ এখানকার সিদ্ধান্ত পরে বদলানো সবচেয়ে কঠিন।

সুবিধা ১ — নাম। Constructor-এর নাম সবসময় class-এর নাম, তাই দুটি constructor শুধু parameter-এর টাইপ দিয়ে আলাদা হতে পারে। new Temperature(30) সেলসিয়াস না ফারেনহাইট? Temperature.ofCelsius(30) পড়লেই বোঝা যায়। একই টাইপের দুটি ভিন্ন অর্থ constructor দিয়ে প্রকাশই করা যায় না।

সুবিধা ২ — প্রতিবার নতুন object বাধ্যতামূলক নয়। new সবসময় নতুন object বানায়; factory চাইলে আগেরটা ফেরত দিতে পারে। Boolean.valueOf(true) সবসময় একই Boolean.TRUE দেয়, আর Integer.valueOf ছোট সংখ্যার object cache রাখে (পর্ব ৬)। Immutable object-এ এতে মেমরি আর সময় দুটিই বাঁচে।

সুবিধা ৩ — ফেরত টাইপের স্বাধীনতা। Factory ঘোষিত টাইপের যেকোনো subtype ফেরত দিতে পারে, এমনকি input অনুযায়ী আলাদা class. List.of(...) কোন class-এর object দেয়, তা নথিতে বলা নেই — JDK পরের সংস্করণে বদলাতে পারে, আপনার কোড টেরও পাবে না। EnumSet-এর factory enum ছোট হলে এক ধরনের, বড় হলে আরেক ধরনের implementation দেয়।

অসুবিধা। Bloch নিজেই দুটি বলেছেন। শুধু private constructor আর factory থাকলে class-টিকে subclass করা যায় না (যদিও তিনি নিজেই বলেছেন, এটা হয়তো আড়ালে একটি সুবিধা — inheritance-এর বদলে composition-এ ঠেলে দেয়)। আর Javadoc-এ constructor আলাদা করে চোখে পড়ে, factory হারিয়ে যায় অন্য সব static method-এর ভিড়ে — তাই প্রচলিত নাম (of, valueOf, from, getInstance) মেনে চলা জরুরি।

মূল উপলব্ধি: Constructor একটি কঠোর চুক্তি — “নতুন object, ঠিক এই class-এর, এই নামে।” Static factory সেই চুক্তিকে নরম করে নাম, cache আর লুকানো implementation-এর সুযোগ দেয়। তাই সাধারণ নিয়ম: constructor দিয়ে শুরু করুন, কিন্তু নাম, cache বা গোপনীয়তার দরকার হলে factory-র কথা ভাবুন।

প্র ০৩Telescoping constructor কেন সমস্যা, আর “খালি constructor + setter” (JavaBeans ধরন) কেন তার ভালো সমাধান নয়? Builder কীভাবে দুটির সমস্যা মেটায়?

Telescoping-এর জন্ম। ধরুন একটি খাবারের প্যাকেটের পুষ্টি-তথ্য: পরিবেশন আকার আর সংখ্যা বাধ্যতামূলক, ক্যালরি, চর্বি, সোডিয়াম, শর্করা ঐচ্ছিক। Effective Java-র Item 2 ঠিক এই উদাহরণ দিয়ে শুরু করে। প্রথম চেষ্টা: দুই parameter-এর constructor, তিনের, চারের… প্রতিটি আগেরটিকে this(...) দিয়ে ডেকে একটি করে ডিফল্ট জোড়ে। দেখতে দূরবীনের নলের মতো ক্রমশ লম্বা — তাই নাম telescoping. সমস্যা: new Food(240, 8, 100, 0, 35, 27) পড়ে কেউ বলতে পারে না কোন সংখ্যা কী। দুটি int জায়গা অদলবদল হলে compiler কিছু বলে না — ভুল ধরা পড়ে প্যাকেটে ভুল তথ্য ছাপা হওয়ার পরে।

JavaBeans-এর ভুল সমাধান। দ্বিতীয় চেষ্টা: খালি constructor, তারপর setCalories(100), setFat(0)… পড়তে সুন্দর, কারণ প্রতিটি মানের সাথে নাম আছে। কিন্তু এই পর্বের মূল প্রতিশ্রুতি ভেঙে যায়: setter-গুলোর মাঝখানে object অসম্পূর্ণ, আর কোনো একটি জায়গা নেই যেখানে “সব মান একসাথে বৈধ কি না” যাচাই করা যায় (যেমন “চর্বি মোট ওজনের বেশি হতে পারে না” — দুটি field-এর সম্পর্ক)। তার ওপর setter থাকলে object আর immutable রাখা যায় না, ফলে thread-এর মধ্যে ভাগ করা ঝুঁকিপূর্ণ (পর্ব ৪০)।

Builder — দুটির সুবিধা একসাথে। Builder একটি আলাদা, সহকারী object. তার setter-ধরনের method-গুলো নামসহ মান নেয় (JavaBeans-এর পড়ার সুবিধা), কিন্তু আসল object জন্মায় শুধু build()-এ, যেখানে সব মান একসাথে যাচাই হয় আর একটি constructor-এ যায় (telescoping-এর নিরাপত্তা)। আসল object-এ setter নেই, field final — immutable. খরচ হলো একটি বাড়তি class আর কিছু কোড; তাই চার-পাঁচটির কম parameter-এ সাধারণত builder লাগে না। বাস্তবে অনেক দল Lombok নামের library-র @Builder annotation দিয়ে এই কোড নিজে থেকে তৈরি করায়।

অন্য ভাষার উত্তর। Kotlin, Python, C#-এ named argument আর default মান আছে — Food(servingSize = 240, calories = 100) — যা builder-এর বেশিরভাগ কাজ ভাষার স্তরেই করে দেয়। Java-তে এগুলো নেই (পর্ব ১২-এর ভাবনার প্রশ্ন ২), তাই builder Java-তে বিশেষ জনপ্রিয়।

মূল উপলব্ধি: ভালো নকশা দুটি জিনিস একসাথে চায় — পড়ার স্বচ্ছতা আর জন্মের মুহূর্তে পূর্ণ বৈধতা। Telescoping দ্বিতীয়টি দেয় প্রথমটি হারিয়ে, JavaBeans প্রথমটি দেয় দ্বিতীয়টি হারিয়ে; builder দুটিই দেয়, কিছু বাড়তি কোডের বিনিময়ে।

প্র ০৪Constructor-এর ভেতর থেকে এমন method ডাকা, যা subclass override করতে পারে — কেন বিপজ্জনক? আধা-তৈরি subclass object কীভাবে তৈরি হয়?

Inheritance পর্ব ১৯-এ আসবে, কিন্তু ধারণাটি এখনই বোঝা যায়। অংশ ২-এর ক্রম মনে করুন: যেকোনো constructor-এর শুরুতে আগে parent-এর constructor চলে, তারপর আপনার class-এর field initializer, তারপর আপনার constructor-এর শরীর। আর Java-তে instance method-এর ডাক চলার সময় ঠিক হয় — object আসলে যে class-এর, তার সংস্করণ চলে (পর্ব ২০-এর dynamic dispatch). এই দুটি নিয়ম একসাথে একটি ফাঁদ তৈরি করে।

ফাঁদটি। ধরুন parent class Shop-এর constructor শেষে printWelcome() ডাকে। Subclass TeaStall সেই method override করে নিজের একটি field ব্যবহার করে — যেমন private final List<String> menu = new ArrayList<>(); থেকে menu.size(). এখন new TeaStall(): প্রথমে Shop-এর constructor চলে, সে printWelcome() ডাকে, আর dynamic dispatch-এর কারণে চলে TeaStall-এর সংস্করণ। কিন্তু TeaStall-এর field initializer তখনো চলেনি! menu এখনো null, তাই NullPointerException. final field-ও এই মুহূর্তে তার ডিফল্ট মান দেখায় — final-এর “একবারই মান” নিশ্চয়তা এই ফাঁক দিয়ে গলে যায়।

কেন কঠিন bug. Parent class-এর লেখক কোনো ভুল করেননি বলে মনে হয়, subclass-এর লেখকও না — প্রত্যেকের কোড আলাদাভাবে ঠিক। ভুলটা দুটির মিলনে, আর stack trace দেখায় parent-এর constructor-এর ভেতরে subclass-এর method — নতুনদের জন্য বিভ্রান্তিকর। Bloch Effective Java-র inheritance-এর জন্য নকশা নিয়ে Item-এ স্পষ্ট নিয়ম দিয়েছেন: constructor কখনো সরাসরি বা পরোক্ষে overridable method ডাকবে না। IntelliJ-এর মতো IDE-ও এমন ডাক দেখলে সতর্ক করে।

প্রতিকার। Constructor থেকে শুধু private, final বা static method ডাকুন — এগুলো override হয় না। অথবা class-টিকেই final করুন। “জন্মের পরে কিছু করা” দরকার হলে static factory-তে করুন: আগে object পুরো তৈরি, তারপর factory তার ওপর method ডাকে। Java 22–24-এ preview থাকার পর Java 25-এ স্থায়ী হওয়া JEP 513 (Flexible Constructor Bodies) subclass-কে super(...)-এর আগেই নিজের field-এ মান দেওয়ার সুযোগ দিয়েছে — ঠিক এই সমস্যার একটি অংশ মেটাতে; Java 21-এ সেটা নেই।

মূল উপলব্ধি: Constructor চলার সময় object এখনো “তৈরি হচ্ছে” — বিশেষ করে subclass অংশটি। সেই সময় বাইরের বা subclass-এর কোডকে ঢুকতে দেওয়া মানে আধা-তৈরি object দেখিয়ে ফেলা, যা constructor-এর মূল প্রতিশ্রুতির উল্টো।

প্র ০৫C++-এ member initializer list (: name(n), price(p)) প্রায় বাধ্যতামূলক অভ্যাস। Java-তে এমন কিছু নেই কেন, আর তাতে কী হারায় বা পায়?

C++-এ কেন লাগে। C++-এ একটি object-এর ভেতরে অন্য object সরাসরি থাকতে পারে — শুধু ঠিকানা নয়, পুরো object. Constructor-এর শরীর শুরুর আগেই প্রতিটি member-কে তৈরি হতে হয়। Initializer list না দিলে member-টি আগে তার default constructor দিয়ে তৈরি হয়, তারপর শরীরে আবার assign — দুবার কাজ। আর const member, reference member বা default constructor-হীন member-এর ক্ষেত্রে শরীরে assign করা অসম্ভব — তাদের জন্য initializer list একমাত্র পথ। একটি সূক্ষ্ম ফাঁদও আছে: member-গুলো তৈরি হয় class-এ ঘোষণার ক্রমে, list-এ লেখার ক্রমে নয়।

Java-তে কেন লাগে না। Java-তে primitive ছাড়া প্রতিটি field একটি reference — ঠিকানা রাখার একটি ছোট ঘর। Object-এর মেমরি নেওয়ার সময়েই সব ঘর শূন্য বা null হয়ে যায় (অংশ ২-এর ধাপ ১), কোনো constructor চলে না। তাই “আগে default তৈরি, পরে আবার assign”-এর খরচ নেই — একটি ঠিকানা লেখা মাত্র। আর C++-এর const member-এর কাজ Java-তে করে final field, যেখানে compiler-এর definite assignment বিশ্লেষণ নিশ্চিত করে যে প্রতিটি constructor-এর প্রতিটি পথে ঠিক একবার মান বসে। ফলে সাধারণ assignment-ই যথেষ্ট, আলাদা syntax লাগে না।

Java কী পেল। সরলতা: একটি ধরনের syntax, আর field initializer ও block-এর জন্য “লেখার ক্রমেই চলে” — সহজ নিয়ম। অনির্ধারিত (garbage) মান অসম্ভব, কারণ সব আগে থেকেই শূন্য।

Java কী হারাল। শূন্য করে দেওয়ার একটি খরচ আছে, যদিও JVM সাধারণত বড় ব্লক একসাথে শূন্য করে সেটা কমায়। আর ধারণাগত দিক থেকে, “ডিফল্ট মান”-এর একটি মুহূর্ত থেকেই যায় — প্র ০৪-এর ফাঁদটি ঠিক সেই মুহূর্তের সুযোগ নেয়, যেখানে final field-কেও তার ডিফল্ট মানে দেখা যায়। C++-এ সঠিক initializer list থাকলে member কখনো “অপ্রস্তুত” অবস্থায় দেখা যায় না (যদিও C++-এর নিজের ফাঁদ আছে)।

মূল উপলব্ধি: Initializer list একটি সমস্যার সমাধান — object-এর ভেতরে object সরাসরি থাকা। Java সেই সমস্যাটিই রাখেনি (সব কিছু reference), তাই সমাধানেরও দরকার পড়েনি; বদলে শূন্য-করা মেমরি আর final-এর compile-time যাচাই দিয়ে একই নিরাপত্তা দিয়েছে।

অনুশীলন

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

  1. নিয়ম ভাঙুন: অংশ ৫-এর Ticket প্রোগ্রামে Ticket(String route)-এর দুটি লাইনের ক্রম উল্টে দিন — আগে println, তারপর this(route, 0);. চালালে কী হবে? তারপর main-এ new Ticket("Dhaka-Khulna", 12) দিয়ে দেখুন কয়টি লাইন ছাপা হয়।

    ক্রম উল্টালে compile error: call to this must be first statement in constructor (Java 21). this(...) সবসময় constructor-এর প্রথম statement হতে হয়। ক্রম ঠিক করে new Ticket("Dhaka-Khulna", 12) চালালে শুধু একটি লাইন ছাপা হয় — 1: Ticket(route, seat) — কারণ দুই-parameter constructor সরাসরি মিলেছে, শিকলের ওপরের দুটি constructor-কে কেউ ডাকেনি। শিকল সবসময় ওপর থেকে নিচে নামে, নিচ থেকে ওপরে ওঠে না।

  2. কোন যাচাই আগে ধরবে? অংশ ৬-এর Product প্রোগ্রামে new Product("", -5, -1) দিলে ঠিক কোন message আসবে? তারপর new Product(null, 100, 5)-এ কী হবে — NullPointerException নাকি IllegalArgumentException? আগে অনুমান, তারপর চালিয়ে মিলিয়ে নিন।

    প্রথমটিতে Rejected: name is empty — তিনটি মানই ভুল, কিন্তু যাচাইগুলো ওপর থেকে নিচে চলে, আর প্রথম throw-এই constructor থেমে যায়। দ্বিতীয়টিতেও IllegalArgumentException (“name is empty”), NullPointerException নয় — কারণ name == null || name.isBlank()-এ || short-circuit (পর্ব ৭): প্রথম অংশ সত্য হলে দ্বিতীয় অংশ (isBlank()) ডাকাই হয় না। শর্তের ক্রম উল্টে name.isBlank() || name == null লিখলে null-এ NullPointerException আসত। অনেক কোডে null-এর জন্য আলাদা করে Objects.requireNonNull(name, "name") ব্যবহার হয়, যা ইচ্ছা করেই NullPointerException ছোড়ে — দুই রীতিই প্রচলিত।

  3. Copy constructor লিখুন: অংশ ৬-এর Product-এ একটি copy constructor যোগ করুন, যা যাচাই-সহ আসল constructor-কে কাজ দেয়। main-এ একটি পণ্য কপি করে দুটিই ছাপুন। ভাবুন: field-এ যদি একটি List<String> tags থাকত, কী বাড়তি করতে হতো?
    Product(Product other) {
        this(other.name, other.price, other.stock);
    }
    
    // main-এ:
    Product a = new Product("Chal 5kg", 420.0, 30);
    Product b = new Product(a);
    System.out.println(a);
    System.out.println(b);

    দুটি লাইনই Chal 5kg | 420.0 tk | stock 30 ছাপে, কিন্তু a আর b দুটি আলাদা object — a == b হলো false. other.name private হলেও পড়া যায়, কারণ private-এর সীমা class, object নয় — একই class-এর কোড অন্য object-এর private field দেখতে পারে (পর্ব ১৭)। List থাকলে this.tags = new ArrayList<>(other.tags); লিখে নতুন list বানাতে হতো, নইলে দুটি পণ্য একই list ভাগ করত আর একটিতে tag যোগ করলে অন্যটিতেও দেখা যেত (shallow copy). String immutable, তাই তাকে কপি করার দরকার নেই।

  4. ফাঁক খুঁজুন: ধরুন Product-এর price থেকে final সরিয়ে একটি method যোগ করা হলো: void setPrice(double price) { this.price = price; }. Constructor-এর যাচাই কি এখনো object-কে বৈধ রাখার নিশ্চয়তা দেয়? কোন দুটি উপায়ে ফাঁক বন্ধ করবেন?

    না। Constructor শুধু জন্মের মুহূর্ত পাহারা দেয়; জন্মের পরে p.setPrice(-50) চুপচাপ দাম ঋণাত্মক করে দেবে — invariant ভাঙল। উপায় ১: setter-এও একই যাচাই রাখুন (আর সূত্র এক জায়গায় রাখতে constructor আর setter দুজনেই একটি private যাচাই-method ডাকুক)। উপায় ২: setter-ই বাদ দিন — দাম বদলালে নতুন object ফেরত দিন, যেমন Product withPrice(double newPrice) { return new Product(name, newPrice, stock); }, যা constructor-এর যাচাই দিয়েই যায়; তখন class immutable থাকে। কোনটা কখন ভালো, encapsulation-এর পর্বে (পর্ব ১৭) আর record-এর পর্বে (পর্ব ২৫) আরও দেখবেন।

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

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

ক্রমJava-তে new দিয়ে একটি object তৈরির ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧱 heap-এ জায়গা নেওয়া হয়, সব field ডিফল্ট মান পায় (0, false, null)
  2. 📞 constructor ডাকা হয়, শুরুতে parent-এর constructor (super) চলে
  3. 📝 field initializer আর instance initializer block লেখার ক্রমে চলে
  4. ⚙️ constructor-এর বাকি শরীর চলে — যাচাই আর this.field = …
  5. ↩️ new নতুন object-এর reference ফেরত দেয়, ভ্যারিয়েবলে বসে

class A-তে শুধু একটি constructor আছে: A(int x). অন্য কোথাও new A() লিখলে কী হবে?

  • compiler-এর default constructor চলবে, x হবে 0
  • A(int x) চলবে, x-এ 0 বসবে
  • compile error, কারণ argument-হীন constructor নেই
  • চলার সময় NullPointerException

class Box-এর ভেতরে লেখা আছে: void Box() { System.out.println("hi"); } — এটি কী?

  • Box-এর argument-হীন constructor, new Box() লিখলে hi ছাপবে
  • একটি সাধারণ method, যার নাম class-এর নামের মতো
  • compile error, কারণ method-এর নাম class-এর নাম হতে পারে না
  • static initializer, class load হওয়ার সময় একবার চলে

Java 21-এ একই class-এর আরেকটি constructor ডাকার জন্য this(...) কোথায় লেখা যায়?

  • class-এর যেকোনো method-এর ভেতরে
  • constructor-এর যেকোনো লাইনে
  • constructor-এ super(...)-এর ঠিক পরে
  • শুধু constructor-এর প্রথম statement হিসেবে

class P { String name; P(String name) { name = name; } } — new P("Rina").name-এর মান কী?

  • null
  • "Rina"
  • compile error
  • "" (ফাঁকা String)

class C { int x = 5; C() { x = x * 2; } } — new C().x-এর মান কী?

  • 0
  • 5
  • 10
  • compile error

Java-তে একটি object-এর দিকে আর কোনো reference না থাকলে তার মেমরি কে ফেরত নেয়?

  • class-এর destructor, যা নিশ্চিতভাবে তখনই চলে
  • garbage collector, কোনো এক সময়ে
  • finalize() method, প্রতিবার সঙ্গে সঙ্গে
  • প্রোগ্রামারকে delete লিখে মুক্ত করতে হয়

শব্দকোষ

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

মনে রাখুন

  1. Constructor-এর নাম class-এর নামে, কোনো return type নেই (void লিখলে সেটা method), আর new-এর সাথে ঠিক একবার চলে — যাতে object জন্ম থেকেই বৈধ থাকে।
  2. তৈরির ক্রম: ডিফল্ট মান → super() → field initializer ও block (লেখার ক্রমে) → constructor-এর শরীর → reference ফেরত।
  3. Default constructor আসে শুধু তখন, যখন একটিও constructor লেখা নেই; একটি লিখলেই হারায় — cannot be applied to given types.
  4. this = বর্তমান object: this.name = name shadowing মেটায়, this(...) প্রথম statement হিসেবে আরেক constructor ডাকে, return this method chaining দেয়।
  5. যাচাই constructor-এ (IllegalArgumentException), স্থির field final; কপিতে copy constructor, নামের দরকারে static factory, অনেক parameter-এ builder; destructor নেই — GC আর try-with-resources, finalize নয়।

পরের পর্বে: constructor জন্মের মুহূর্ত পাহারা দেয় — কিন্তু জন্মের পরে কেউ সরাসরি p.price = -50 লিখলে? private, public, protected দিয়ে field লুকানো, getter-setter কখন দরকার আর কখন নয়, আর encapsulation কীভাবে object-এর invariant সারাজীবন রক্ষা করে।

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

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