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

Java method — overloading, varargs আর pass-by-value

Java Methods — Parameters, Return, the Call Stack, Overloading, Varargs and Pass-by-Value

Taught in Bangla, this lesson explains Java methods: signatures, return types and void, local variables and call-stack frames, overloading and how the compiler resolves overloads, varargs rules, why Java is always pass-by-value even for objects, recursion and StackOverflowError, and writing small, well-named methods with Javadoc.

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

  • static method লেখা ও ডাকা — signature, return type, void, return আর compiler-এর আসল error মেসেজ
  • Local variable, scope আর call stack — প্রতিটি ডাকের নিজস্ব frame
  • Overloading আর overload resolution, varargs-এর নিয়ম
  • Pass-by-value-এর আসল মানে — primitive, object আর “কাজ না করা swap”
  • Recursion-এর শুরু, StackOverflowError, Javadoc আর ভালো method-এর নাম ও আকার

১ · Method — নাম দেওয়া একটি রেসিপি

পর্ব ১১ পর্যন্ত আপনার সব কোড ছিল main-এর ভেতরে। ছোট প্রোগ্রামে এতে সমস্যা নেই। কিন্তু একটি দোকানের বিলিং প্রোগ্রাম ভাবুন — দশ জায়গায় VAT হিসাব হয়, পাঁচ জায়গায় টাকা থেকে ডলার। একই তিন লাইন দশবার কপি করলে কী হয়? VAT-এর হার বদলালে দশ জায়গায় বদলাতে হবে, আর একটি জায়গা ভুলে গেলেই হিসাব গরমিল।

সমাধান: কাজটিকে একবার লিখে একটি নাম দিন, তারপর যতবার দরকার নাম ধরে ডাকুন। Java-তে এই নাম দেওয়া কাজের টুকরোকে বলে method. C বা Python-এ একে বলে function; Java-তে প্রতিটি function কোনো না কোনো class-এর ভেতরে থাকে, তাই নাম method.

মায়ের খিচুড়ির রেসিপি ভাবুন। রেসিপির একটি নাম আছে (“খিচুড়ি”), কিছু উপকরণ লাগে (চাল, ডাল, কতজনের জন্য), আর শেষে একটি জিনিস পাওয়া যায় (এক হাঁড়ি খিচুড়ি)। রেসিপি একবার লেখা থাকে, রান্না হয় বহুবার — প্রতিবার আলাদা উপকরণ দিয়ে। Method ঠিক তাই: নাম, উপকরণ (parameter), আর ফল (return value). যিনি রান্না করতে বলছেন, তাঁর জানার দরকার নেই ভেতরে ঠিক কীভাবে ফোড়ন দেওয়া হলো।

একটি method-এর শরীরের প্রতিটি অংশ দেখুন:

static double addVat(double price, double rate) {
    double vat = price * rate;
    return price + vat;
}

static

Object ছাড়াই, class-এর নাম দিয়ে ডাকা যায়। আপাতত সব method এভাবেই লিখব; object-এর নিজস্ব (instance) method শুরু হবে পর্ব ১৫-এ।

পর্ব ১৮-এ বিস্তারিত

double (প্রথমটি)

Return type — কাজ শেষে কোন টাইপের মান ফেরত আসবে। কিছু না দিলে void.

ফলের টাইপ

addVat(double price, double rate)

নাম আর parameter-তালিকা। প্রতিটি parameter একটি ভ্যারিয়েবল, যার প্রথম মান আসে ডাকার সময়।

উপকরণ

{ … return … }

শরীর — আসল কাজ। return ফল পাঠিয়ে দেয় আর method সেখানেই শেষ।

রান্নার ধাপ

ডাকার সময় বন্ধনীর ভেতরে যে মান দেন, তাকে বলে argument: addVat(1000, 0.15)-এ 1000 আর 0.15. Parameter হলো রেসিপিতে লেখা “চাল — কত কেজি”; argument হলো আজ আপনি আসলে কত কেজি দিলেন। শব্দ দুটি প্রায়ই গুলিয়ে ফেলা হয়, কিন্তু পার্থক্যটা অংশ ৭-এ খুব কাজে লাগবে।

Signature — compiler method চেনে কী দিয়ে

Java-তে একটি method-এর পরিচয় হলো তার নাম আর parameter-গুলোর টাইপের ক্রম — একে বলে method signature. ওপরের method-এর signature হলো addVat(double, double). খেয়াল করুন, তিনটি জিনিস signature-এর অংশ নয়: return type, parameter-এর নাম (price না p, তাতে কিছু যায় আসে না), আর static-এর মতো modifier. এই তথ্যটিই অংশ ৪-এ overloading-এর নিয়ম ঠিক করে দেবে।

নাম রাখার রীতি — ক্রিয়াপদ, camelCase

Method কিছু করে, তাই তার নাম সাধারণত একটি ক্রিয়াপদ দিয়ে শুরু হয়: addVat, calculateGpa, printReceipt, convertToDollar. হ্যাঁ/না উত্তর দিলে is বা has দিয়ে: isPassed, hasDiscount. লেখার রীতি camelCase — প্রথম শব্দ ছোট হাতের, পরের প্রতিটি শব্দের প্রথম অক্ষর বড়। Add_VAT বা addvat লিখলেও compile হবে, কিন্তু যেকোনো Java দলের code review-তে আটকে যাবে।

মূল ধারণা

একটি ভালো method একটি কাজ করে, তার নাম সেই কাজটিই বলে, আর signature দেখেই বোঝা যায় কী দিতে হবে আর কী পাওয়া যাবে। addVat(price) পড়ে ভেতরে না তাকিয়েই আপনি জানেন কী ঘটবে — এটাই method-এর আসল শক্তি: জটিলতা একটি নামের পেছনে লুকিয়ে রাখা।

২ · return, void আর local variable

return statement দুটি কাজ করে একসাথে: একটি মান ডাকার জায়গায় পাঠায়, আর method-কে সেই মুহূর্তে শেষ করে। return-এর পরের লাইন আর চলে না। void method-এ মান ছাড়া শুধু return; লিখে আগেভাগে বের হওয়া যায়, আর শেষ লাইনে পৌঁছালে এমনিতেই শেষ হয়।

Java-র compiler এখানে কড়া পাহারাদার। যে method double ফেরত দেওয়ার প্রতিশ্রুতি দিয়েছে, তার প্রতিটি সম্ভাব্য পথে একটি return থাকতেই হবে। নিচের GPA method-টি দেখতে ঠিকঠাক, কিন্তু compile হয় না:

static double gradePoint(int marks) {
    if (marks >= 80) return 4.0;
    else if (marks >= 70) return 3.5;
    else if (marks >= 60) return 3.0;
}   // ৬০-এর কম হলে কী ফেরত যাবে?
Main.java:6: error: missing return statement
    }
    ^
1 error

Compiler বলছে: “marks যদি ৫৫ হয়, তাহলে তিনটি শর্তই মিথ্যা, আর তখন কিছুই ফেরত যায় না।” মানুষ ভাবে “এমন নম্বর তো আসবে না”; compiler ভাবে “আসতে পারে, তাই প্রমাণ দাও।” সমাধান — শেষে একটি else return 0.0; বা শুধু return 0.0;. এই কড়াকড়ি বিরক্তিকর মনে হতে পারে, কিন্তু C-তে একই ভুলে প্রোগ্রাম নীরবে আবর্জনা মান ফেরত দেয় (undefined behavior) — Java সেটা চলার আগেই আটকায়।

আরও দুটি সম্পর্কিত error, যা নতুনরা প্রায়ই দেখেন (সবগুলো Wandbox-এর OpenJDK 21-এ যাচাই করা):

কোডjavac যা বলেমানে
void greet() { return "hi"; }incompatible types: unexpected return valuevoid method কিছু ফেরত দিতে পারে না
return 1;-এর পরে আরেকটি লাইনunreachable statementএই লাইনে কোনোদিন পৌঁছানো যাবে না
int t; return t;variable t might not have been initializedLocal variable-এ মান না দিয়ে পড়া যায় না

Local variable আর scope

Method-এর ভেতরে ঘোষণা করা ভ্যারিয়েবল — যেমন ওপরের vat — হলো local variable. এটি শুধু ওই method-এর, বাইরে থেকে দেখা যায় না। কোন অংশ থেকে একটি নাম দেখা যায়, তাকে বলে scope: local variable-এর scope তার ঘোষণার লাইন থেকে যে block-এ সে আছে, তার শেষ } পর্যন্ত। main-এ int sub = 5; লিখে অন্য method থেকে sub পড়তে গেলে javac বলে cannot find symbol.

এর একটি সুন্দর ফল আছে: দুটি আলাদা method-এ একই নাম total রাখলে কোনো সংঘর্ষ নেই। প্রতিটি method নিজের ঘরে কাজ করে। আর Java-র আরেকটি নিয়ম, যা C-তে নেই — local variable-এর কোনো ডিফল্ট মান নেই। মান না দিয়ে পড়লে compile error, যাতে ভুল করে “আবর্জনা” পড়া অসম্ভব হয়।

method-এর ভেতরে method? Java-তে একটি method-এর ভেতরে আরেকটি সাধারণ method লেখা যায় না — সব method থাকে class-এর সরাসরি ভেতরে, পাশাপাশি। কোন ক্রমে লিখছেন তাতেও কিছু যায় আসে না: main নিচে থেকেও ওপরের বা নিচের যেকোনো method ডাকতে পারে, C-র মতো আগে prototype লেখার ঝামেলা নেই।

৩ · Call stack — প্রতিটি ডাকের নিজের খাতা

একটি method আরেকটিকে ডাকে, সেটি আবার আরেকটিকে। JVM কীভাবে মনে রাখে কে কাকে ডেকেছে, আর কাজ শেষে কোথায় ফিরতে হবে? পর্ব ০৫-এ দেখেছিলেন, প্রতিটি thread-এর একটি নিজস্ব stack থাকে। এই stack-কেই বলে call stack. প্রতিটি method-ডাকের জন্য এতে একটি নতুন খাতা খোলে — একে বলে stack frame. খাতায় থাকে সেই ডাকের parameter, local variable, আর কাজ শেষে কোন লাইনে ফিরতে হবে সেই তথ্য।

অফিসের টেবিলে ফাইলের স্তূপ। ম্যানেজার একটি ফাইল (main) নিয়ে কাজ করছেন। মাঝপথে মোট বিল লাগল — ওপরে রাখলেন আরেকটি ফাইল (total). সেটার কাজে আবার VAT লাগল — আরও একটি ফাইল (addVat). সবার ওপরেরটা শেষ হলে সেটা সরিয়ে ঠিক নিচেরটায় ফিরে যান। যেটা শেষে রাখা হয়েছে, সেটাই আগে শেষ হয়।

নিচে main ডাকছে total-কে, আর total ডাকছে addVat-কে। “পরে” চাপুন — প্রতিটি ধাপে stack-এ কোন frame আছে আর কোন ভ্যারিয়েবল বেঁচে আছে, দেখুন:

1static double addVat(double amount) {2    double vat = amount * 0.15;3    return amount + vat;4}5static double total(double price, int qty) {6    double sub = price * qty;7    return addVat(sub);8}9public static void main(String[] args) {10    double bill = total(400, 2);11    System.out.println(bill);12}
প্রতিটি frame-এর ভ্যারিয়েবল
main: bill
total: price
total: qty
total: sub
addVat: amount
addVat: vat
  1. শুরুতে শুধু main. JVM main-এর frame তৈরি করেছে, তাতে bill-এর জায়গা আছে, মান এখনো আসেনি। লাইন ১০-এ total(400, 2) ডাকা হচ্ছে।
  2. total-এর নতুন frame. Argument 400 আর 2 কপি হয়ে বসল parameter price আর qty-তে। খেয়াল করুন, 400 ছিল int, কিন্তু parameter double — তাই Java নিজেই একে 400.0 বানিয়েছে (widening, অংশ ৬-এ আবার আসবে)। main থেমে আছে লাইন ১০-এর মাঝখানে।
  3. Local variable জন্মাল। sub শুধু total-এর frame-এ থাকে, মান 800.0. main থেকে sub নামটি দেখাই যায় না।
  4. আরও এক ধাপ গভীরে। addVat(sub) ডাকায় তৃতীয় frame. sub-এর মান কপি হলো amount-এ। এখন stack-এ তিনটি frame; শুধু ওপরেরটিই সক্রিয়।
  5. VAT হিসাব। vat = 800.0 × 0.15 = 120.0. এই vat শুধু addVat-এর frame-এ বাঁচে।
  6. addVat শেষ, frame মুছে গেল। 920.0 ফেরত গেল ঠিক যেখান থেকে ডাকা হয়েছিল — লাইন ৭-এ। amount আর vat-এর আর কোনো অস্তিত্ব নেই। total সেই মানটিই নিজে return করছে।
  7. total-ও শেষ। 920.0 বসল bill-এ, ছাপা হলো 920.0. Stack আবার শুধু main. শেষে যে এসেছিল (addVat), সে-ই আগে গেছে — LIFO, Last In First Out.

এই ছবিটি মাথায় রাখলে তিনটি জিনিস একসাথে বোঝা যায়। (১) Local variable কেন return-এর পর হারিয়ে যায় — তার খাতাটাই নেই। (২) একই নামের local variable দুটি method-এ কেন সংঘর্ষ করে না — আলাদা খাতা। (৩) Exception হলে যে stack trace ছাপা হয় (at Main.addVat(...), at Main.total(...), at Main.main(...)), সেটা আসলে এই stack-এর ছবি, ওপর থেকে নিচে। পর্ব ২৯-এ exception শেখার সময় এটা আবার কাজে লাগবে।

৪ · Overloading — এক নাম, ভিন্ন parameter-তালিকা

বেশিরভাগ পণ্যে VAT ১৫%, কিন্তু কিছু পণ্যে ৫% বা ৭.৫%. দুটি আলাদা নাম রাখবেন — addVat আর addVatWithRate? Java-তে দরকার নেই। একই নামে দুটি method রাখা যায়, যদি তাদের parameter-তালিকা আলাদা হয়। একে বলে method overloading. Parameter-এর সংখ্যা আলাদা হতে পারে, টাইপ আলাদা হতে পারে, বা টাইপের ক্রম।

বাংলায় “চালানো” শব্দটি — রিকশা চালানো, দোকান চালানো, কম্পিউটার চালানো। শব্দ এক, কিন্তু কী চালাচ্ছেন তা দেখে আপনি অর্থ বুঝে যান। Compiler-ও তাই করে: নাম এক, কিন্তু argument কয়টি আর কী ধরনের, তা দেখে বুঝে নেয় কোন addVat ডাকা হচ্ছে।

নিচের প্রোগ্রামে overloaded VAT method, যত খুশি দামের যোগফল (varargs — অংশ ৫-এ ব্যাখ্যা), আর টাকা থেকে ডলার রূপান্তর একসাথে আছে। চালিয়ে দেখুন:

class Main {
    static double addVat(double price) {
        return addVat(price, 0.15);           // সাধারণ হার ১৫%
    }
    static double addVat(double price, double rate) {
        return price + price * rate;
    }
    static double sum(double... prices) {
        double total = 0;
        for (double p : prices) total += p;
        return total;
    }
    static double takaToDollar(double taka) {
        return taka / 120.0;                  // ধরে নেওয়া হার
    }
    public static void main(String[] args) {
        System.out.printf("VAT 15%%: %.2f%n", addVat(1000));
        System.out.printf("VAT 5%% : %.2f%n", addVat(1000, 0.05));
        System.out.printf("Mot     : %.2f%n", sum(250, 120.5, 80));
        System.out.printf("Khali   : %.2f%n", sum());
        System.out.printf("Dollar  : %.2f%n", takaToDollar(addVat(sum(500, 700))));
    }
}

আউটপুট: VAT 15%: 1150.00, VAT 5% : 1050.00, Mot : 450.50, Khali : 0.00, Dollar : 11.50. একটি argument-এর addVat(1000) গেল প্রথম সংস্করণে, দুটি argument-এর ডাক গেল দ্বিতীয়টিতে। লক্ষ করুন, প্রথম সংস্করণ নিজে হিসাব করে না — দ্বিতীয়টিকে ডেকে দেয়। ফলে VAT-এর সূত্র লেখা আছে মাত্র এক জায়গায়; সূত্র বদলালে একবার বদলালেই চলবে। শেষ লাইনে তিনটি method একটির ভেতরে আরেকটি: আগে sum (1200), তারপর addVat (1380), তারপর takaToDollar (11.50) — ভেতর থেকে বাইরে। এখানে ১ ডলার = ১২০ টাকা শুধু উদাহরণের জন্য ধরা; আসল হার প্রতিদিন বদলায়। printf-এ %% মানে একটি % চিহ্ন (পর্ব ০৯)।

শুধু return type আলাদা হলে overload হয় না

static int    parse(String s) { … }
static double parse(String s) { … }
Main.java:9: error: method parse(String) is already defined in class Main
    static double parse(String s) { return 1.0; }
                  ^

কারণ অংশ ১-এর signature: return type signature-এর অংশ নয়, তাই compiler-এর চোখে দুটিই parse(String) — একই method দুবার। আরেকভাবে ভাবুন: parse("42"); লিখে ফল কোথাও না রাখলে compiler কীভাবে বুঝবে কোনটা চান? ডাকার জায়গায় argument দেখা যায়, ফল কোথায় যাবে তা সবসময় দেখা যায় না। তাই আলাদা ফল চাইলে আলাদা নাম — Java-র নিজের library-ও তাই করে: Integer.parseInt, Double.parseDouble. একই কারণে শুধু parameter-এর নাম বদলালেও (addVat(double p) বনাম addVat(double price)) overload হয় না।

Overloading বনাম overriding: নাম দুটি শুনতে কাছাকাছি, কিন্তু একদম আলাদা জিনিস। Overloading হলো একই class-এ একই নাম, ভিন্ন parameter — compiler compile-এর সময় বেছে নেয়। Overriding হলো subclass-এ parent-এর method হুবহু একই signature-এ নতুন করে লেখা — কোনটা চলবে তা ঠিক হয় চলার সময়। Overriding দেখবেন পর্ব ১৯ আর ২০-এ; ইন্টারভিউতে এই পার্থক্য খুব প্রচলিত প্রশ্ন।

৫ · Varargs — যত খুশি argument

একজন খদ্দের দুটি জিনিস কিনলেন, আরেকজন সাতটি। যোগফলের method-এ কয়টি parameter রাখবেন? sum(double a, double b), sum(double a, double b, double c)… এভাবে অসীম overload লেখা অসম্ভব। Java 5 থেকে এর সমাধান varargs (variable-length arguments): টাইপের পরে তিনটি বিন্দু।

static double sum(double... prices) {   // prices আসলে একটি double[]
    double total = 0;
    for (double p : prices) total += p;
    return total;
}

sum();                    // prices.length == 0
sum(250);                 // 1টি
sum(250, 120.5, 80);      // 3টি
sum(new double[] {1, 2}); // আগে থেকে বানানো array-ও দেওয়া যায়

ভেতরের রহস্য সহজ: compiler ডাকার জায়গায় argument-গুলো দিয়ে একটি array বানিয়ে পাঠায়। Method-এর ভেতরে prices একটি সাধারণ double[] — prices.length, prices[0] সব চলে (array-র পূর্ণ আলোচনা পরের পর্বে)। কোনো argument না দিলে আসে শূন্য দৈর্ঘ্যের array, null নয় — তাই ওপরের লুপ নিরাপদে 0 ফেরত দেয়।

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

  • একটি method-এ বড়জোর একটি varargs parameter. দুটি থাকলে compiler বুঝবে না কোন argument কোন দলে।
  • সেটি থাকতে হবে সবার শেষে। printBill(String shop, double... prices) ঠিক আছে; printBill(double... prices, String shop) compile error — কারণ কোথায় দাম শেষ আর দোকানের নাম শুরু, তা নির্ধারণ করা যায় না।
  • Overload বাছাইয়ে varargs সবার শেষে বিবেচিত হয়। অন্য কোনো method মিললে varargs-ওয়ালাটি জেতে না — পরের অংশে দেখবেন।

আপনি varargs আগে থেকেই ব্যবহার করছেন, হয়তো না জেনে: System.out.printf(String format, Object... args) আর String.format দুটিই varargs method. তাই printf("%s %d", name, age)-এ যত খুশি মান দেওয়া যায়।

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

C/C++-এর মতোই Java-তে overloading আছে আর argument by value যায়; কিন্তু Java-তে C++-এর int& (reference parameter) বা C-র pointer দিয়ে caller-এর ভ্যারিয়েবল বদলানোর কোনো উপায় নেই, আর default argument-ও নেই। Python-এর মতো Java object-এর reference পাঠায় — Python-এ যাকে বলা হয় “pass by assignment”, আচরণ হুবহু এক — কিন্তু Python-এ overloading নেই, আর Java-র varargs Python-এর *args-এর মতো হলেও টাইপ-নির্দিষ্ট। গভীরে তুলনা করতে দেখুন C++-এর ফাংশন ও overloading আর Python-এর function পর্ব।

৬ · Compiler কোনটা বেছে নেয় — overload resolution

Argument-এর টাইপ সবসময় কোনো overload-এর সাথে হুবহু মেলে না। একটি byte পাঠালেন, অথচ print(byte) নেই — আছে print(int), print(long), print(Integer) আর print(Object). কোনটা চলবে? এই বাছাইয়ের নাম overload resolution, আর Java এটা করে তিনটি ধাপে (Java Language Specification-এর §15.12.2):

ধাপকী অনুমতি আছেউদাহরণ
১হুবহু মিল বা widening — ছোট primitive থেকে বড় primitive, অথবা subclass থেকে superclassbyte → int → long → double; String → Object
২ধাপ ১-এ কেউ না মিললে boxing/unboxing-ও চলবেint → Integer; byte → Byte → Object
৩তাতেও না মিললে varargs5 → int...

যে ধাপে প্রথম কোনো প্রার্থী পাওয়া যায়, খোঁজ সেখানেই থামে — পরের ধাপের প্রার্থী যত “কাছের” মনে হোক। আর এক ধাপে একাধিক প্রার্থী থাকলে জেতে সবচেয়ে নির্দিষ্টটি (int আর long দুটিই মিললে int, কারণ int সবসময় long-এ যেতে পারে, উল্টোটা নয়)। Autoboxing মানে primitive-কে তার wrapper class-এর object-এ নিজে থেকে মুড়ে দেওয়া (পর্ব ০৬)। নিচের ডায়াগ্রামে প্রতিটি ধাপে আগে নিজে অনুমান করুন, তারপর “পরে” চাপুন:

1static void print(int x)     { … }2static void print(long x)    { … }3static void print(Integer x) { … }4static void print(Object x)  { … }5byte b = 10;6print(b);7print(5);
কোন overload কোন ধাপে মেলে
ডাক
print(int)
print(long)
print(Integer)
print(Object)
  1. print(b), b একটি byte. হুবহু print(byte) নেই। ধাপ ১-এ widening দিয়ে int আর long দুটিই মেলে। দুটির মধ্যে বেশি নির্দিষ্ট int — তাই print(int) জেতে। ধাপ ১-এ প্রার্থী পাওয়া গেছে, তাই boxing-এর কথা ভাবাই হয়নি।
  2. print(5). 5 একটি int literal — print(int)-এর সাথে হুবহু মিল। সহজ জয়।
  3. এবার print(int) মুছে দিলাম। print(5) কোথায় যাবে? অনেকে বলেন print(Integer) — “int আর Integer তো একই জিনিস।” কিন্তু উত্তর print(long)! কারণ widening (int → long) ধাপ ১-এ পড়ে, আর boxing ধাপ ২-এ। ধাপ ১-এ প্রার্থী পেলে ধাপ ২-এ যাওয়াই হয় না। Java 5-এ boxing যোগ করার সময় এই নিয়ম রাখা হয়েছিল, যাতে পুরোনো কোডের আচরণ না বদলায় — পুরোনো Java-তে এই ডাক long-এই যেত।
  4. print(long)-ও মুছলাম। এখন ধাপ ১-এ কোনো প্রার্থী নেই। ধাপ ২-এ boxing চালু: int → Integer মেলে, int → Integer → Object-ও মেলে। বেশি নির্দিষ্ট Integer জেতে।
  5. একই অবস্থায় print(b) — সবচেয়ে চমকপ্রদ। byte-কে box করলে হয় Byte, Integer নয়। আর Java box করার সাথে সাথে widening করে না (byte → int → Integer পথ নেই)। তাই print(Integer) বাদ, আর Byte যেহেতু একটি Object, জেতে print(Object). পুরো উদাহরণটি OpenJDK 21-এ চালিয়ে যাচাই করা।

একই নিয়মে varargs-ও সবার শেষে আসে: print(Object) আর print(int...) থাকলে print(5) যায় print(Object)-এ (ধাপ ২-এর boxing, varargs-এর আগে), কিন্তু print() বা print(1, 2) যায় varargs-এ, কারণ অন্য কেউ মেলে না।

বাস্তব শিক্ষা: এই ধাঁধাগুলো পরীক্ষায় আসে, কিন্তু নিজের কোডে এমন অবস্থা তৈরি করবেন না। যদি কোনো ডাক কোন overload-এ যাবে তা বুঝতে টেবিল খুলতে হয়, তাহলে overload-গুলোর নাম আলাদা করুন। একই সংখ্যক parameter-ওয়ালা overload-এ long আর Integer, বা Object আর অন্য টাইপ একসাথে রাখা বিশেষ বিপজ্জনক। আর দুটি প্রার্থী সমান নির্দিষ্ট হলে compiler বেছে নেয় না — ambiguous call error দেয়, যেমন f(int, long) আর f(long, int) থাকলে f(1, 2).

৭ · Pass-by-value — Java সবসময় কপি পাঠায়

এবার এই পর্বের সবচেয়ে জরুরি, আর Java দুনিয়ায় সবচেয়ে বেশি ভুল বোঝা বিষয়। নিয়মটি এক লাইনের: Java সবসময় pass by value — argument-এর মানের একটি কপি parameter-এ যায়। ব্যতিক্রম নেই। বিভ্রান্তি আসে একটি প্রশ্ন থেকে: object-এর ক্ষেত্রে “মান” জিনিসটা কী?

পর্ব ০৫-এ দেখেছেন, object থাকে heap-এ, আর ভ্যারিয়েবলে থাকে শুধু তার reference — অনেকটা ঠিকানার মতো। StringBuilder name ভ্যারিয়েবলের “মান” তাই object নয়, ঠিকানাটি। Method ডাকলে সেই ঠিকানাটিই কপি হয়। এর তিনটি ফল আছে, আর তিনটিই একটি প্রোগ্রামে দেখা যাক:

class Main {
    static void makeBigger(int n) {
        n = n * 10;                          // শুধু কপিটা বদলাল
    }
    static void addTitle(StringBuilder name) {
        name.insert(0, "Dr. ");              // একই object-এর ভেতরে বদল
    }
    static void replace(StringBuilder name) {
        name = new StringBuilder("Keu na");  // শুধু local কপি নতুন object দেখাল
    }
    static void swap(Integer a, Integer b) {
        Integer t = a; a = b; b = t;
    }
    public static void main(String[] args) {
        int price = 50;
        makeBigger(price);
        StringBuilder name = new StringBuilder("Rahima");
        addTitle(name);
        replace(name);
        Integer x = 1, y = 2;
        swap(x, y);
        System.out.println("price = " + price);
        System.out.println("name  = " + name);
        System.out.println("x = " + x + ", y = " + y);
    }
}

আউটপুট: price = 50, name = Dr. Rahima, x = 1, y = 2. (১) makeBigger পেল 50-এর একটি কপি, কপিটাকে 500 বানাল, method শেষে কপি মুছে গেল — price অক্ষত। (২) addTitle পেল ঠিকানার কপি, কিন্তু সেই ঠিকানায় গিয়ে একই StringBuilder object-এর ভেতরে “Dr. ” বসাল — তাই main-ও পরিবর্তন দেখে। (৩) replace নিজের parameter-এ নতুন object-এর ঠিকানা বসাল; এতে শুধু তার নিজের কপি বদলাল, main-এর name আগের object-ই দেখাচ্ছে। (৪) swap একই কারণে ব্যর্থ: a আর b শুধু কপি, তাদের অদলবদল x আর y-কে ছোঁয় না। (একটি টীকা: main থাকা class এখানে public ছাড়া লেখা, কারণ অনলাইন runner ফাইলটিকে অন্য নামে compile করে; নিজের প্রজেক্টে সাধারণত public class Main থাকে Main.java ফাইলে।)

তিনটি ঘটনা পাশাপাশি দেখলে ছবিটা পরিষ্কার হয়। প্রতিটি ঘটনার দুটি ধাপ — ডাকের মুহূর্তে, আর parameter বদলানোর পরে:

ঘটনা ১ · primitivemakeBigger(price) ডাকার মুহূর্তে
main-এর frame          makeBigger-এর frame
┌──────────────┐        ┌──────────────┐
│ price = 50   │ ──কপি──▶│ n = 50       │
└──────────────┘        └──────────────┘
দুটি আলাদা বাক্স, দুটিতেই 50
ঘটনা ১ · primitiven = n * 10 করার পরে
main-এর frame          makeBigger-এর frame
┌──────────────┐        ┌──────────────┐
│ price = 50   │         │ n = 500      │
└──────────────┘        └──────────────┘
শুধু কপি বদলাল; return-এ n মুছে যাবে
ঘটনা ২ · referenceaddTitle(name) ডাকার মুহূর্তে
stack                        heap
main:     name ──────┐
                     ├──▶ [ StringBuilder: "Rahima" ]
addTitle: name ──────┘
ঠিকানা কপি হয়েছে — দুটি তীর, একটিই object
ঘটনা ২ · referencename.insert(0, "Dr. ") করার পরে
stack                        heap
main:     name ──────┐
                     ├──▶ [ StringBuilder: "Dr. Rahima" ]
addTitle: name ──────┘
object-এর ভেতরটা বদলাল — main-ও তা-ই দেখে
ঘটনা ৩ · parameter-এ নতুন objectreplace(name) ডাকার মুহূর্তে
stack                        heap
main:    name ──────┐
                    ├──▶ [ StringBuilder: "Dr. Rahima" ]
replace: name ──────┘
শুরুটা ঘটনা ২-এর মতোই
ঘটনা ৩ · parameter-এ নতুন objectname = new StringBuilder(...) করার পরে
stack                        heap
main:    name ─────────▶ [ StringBuilder: "Dr. Rahima" ]
replace: name ─────────▶ [ StringBuilder: "Keu na" ]
শুধু replace-এর তীর ঘুরল; main-এর তীর আগের জায়গায়
  1. Primitive — মান কপি। price-এর বাক্সে আছে সরাসরি সংখ্যা 50. ডাকার সময় সংখ্যাটি কপি হয়ে নতুন বাক্স n-এ বসল। এখন দুটি স্বাধীন বাক্স।
  2. কপি বদলালে আসল বদলায় না। n হলো 500, কিন্তু price-এর বাক্সে কেউ হাত দেয়নি। Method শেষে n-এর frame মুছে যাবে, আর 500 চিরতরে হারাবে। “make it bigger” তাই কিছুই বড় করে না — চাইলে return n * 10; করে ফল ফেরত দিন আর price = makeBigger(price); লিখুন।
  3. Reference — ঠিকানা কপি। name-এর বাক্সে আছে object-এর ঠিকানা। কপি হলো সেই ঠিকানা, object নয়। তাই এখন দুটি ভ্যারিয়েবল একই object-এর দিকে তাকিয়ে আছে — একই বাড়ির ঠিকানা দুটি কাগজে লেখা।
  4. ঠিকানা ধরে ভেতরে বদল — caller দেখে। addTitle ঠিকানায় গিয়ে object-এর ভেতরে “Dr. ” বসাল। main-এর কাগজেও একই ঠিকানা, তাই সে-ও বদলানো object দেখে। এটা দেখেই অনেকে ভাবেন “Java object by reference পাঠায়।” কিন্তু খেয়াল করুন, পাঠানো হয়েছে ঠিকানার কপি।
  5. শুরুটা একই। replace-ও পেল ঠিকানার একটি কপি, দুটি তীর একই object-এ।
  6. Parameter-এ নতুন ঠিকানা — caller টের পায় না। replace নিজের কাগজে নতুন object-এর ঠিকানা লিখল। main-এর কাগজ বদলায়নি, তাই সে আগের object-ই দেখছে। সত্যিকারের pass by reference থাকলে (C++-এর &) এখানে main-এর name-ও নতুন object দেখাত। Java-তে তা হয় না — এটাই প্রমাণ যে Java pass-by-value.

যে swap কাজ করে না

“দুটি ভ্যারিয়েবলের মান অদলবদল করার একটি method লিখুন” — C++-এ void swap(int& a, int& b) দিয়ে এটা সহজ। Java-তে সম্ভবই নয়, primitive হোক বা object. ওপরের swap(Integer a, Integer b) ঘটনা ৩-এর মতোই: a = b শুধু local কপিতে নতুন ঠিকানা বসায়। আর Integer হলো immutable — তার ভেতরের মান বদলানোর কোনো method-ই নেই, তাই ঘটনা ২-এর পথও বন্ধ। Java-তে অদলবদল করতে হয় যেখানে মান থাকে সেখানেই — যেমন একটি array-র দুটি ঘর (arr[i] আর arr[j]), বা একটি object-এর দুটি field, অথবা method থেকে নতুন মান return করে।

Java কপি পাঠায় — সবসময়।
Primitive-এ কপি হয় সংখ্যা, object-এ কপি হয় ঠিকানা।

৮ · Recursion — নিজেকেই ডাকা

একটি method নিজেকেও ডাকতে পারে — একে বলে recursion. শর্ত দুটি: (১) প্রতিটি ডাকে সমস্যা একটু ছোট হবে, (২) একটি সবচেয়ে ছোট অবস্থা থাকবে, যেখানে আর না ডেকে সরাসরি উত্তর দেওয়া যায় — একে বলে base case. ক্লাসিক উদাহরণ factorial: ৩! = ৩ × ২!, আর ১! = ১.

static long factorial(int n) {
    if (n <= 1) return 1;              // base case
    return n * factorial(n - 1);       // ছোট সমস্যা, নিজেকে দিয়ে
}

অংশ ৩-এর call stack-ই এখানে চাবি। প্রতিটি ডাক নিজের frame পায়, নিজের n পায় — তাই একসাথে তিনটি আলাদা n বেঁচে থাকতে পারে:

1static long factorial(int n) {2    if (n <= 1) return 1;3    return n * factorial(n - 1);4}
অবস্থা
stack-এ factorial frame
সর্বশেষ ফেরত মান
  1. factorial(3) শুরু। n = 3, base case নয়। হিসাব 3 * factorial(2) — কিন্তু factorial(2)-এর উত্তর জানা নেই, তাই গুণটা থেমে রইল।
  2. এক ধাপ নিচে। নতুন frame, নিজের n = 2. আগের frame-এর n = 3 হারায়নি, নিচে নিরাপদে আছে। আবার অপেক্ষা: 2 * ?
  3. Base case — থামার জায়গা। n = 1, শর্ত সত্য, তাই আর ডাক নয় — সরাসরি 1 ফেরত। এই লাইন না থাকলে ডাক চলতেই থাকত।
  4. খোলা শুরু। 1 ফিরে গেল factorial(2)-এর অপেক্ষমাণ গুণে: 2 * 1 = 2. সেই frame-ও শেষ, মুছে গেল।
  5. উত্তর। 3 * 2 = 6, ফেরত গেল main-এ। যাওয়ার পথে শুধু প্রশ্ন জমেছে, ফেরার পথে উত্তর তৈরি হয়েছে — recursion বোঝার মূল কথা এটাই।

Base case ভুলে গেলে — StackOverflowError

নিচের কোডে base case নেই। javac কোনো আপত্তি করে না — অসীম recursion ধরা compiler-এর কাজ নয়। কিন্তু চালালে:

class Main {
    static long factorial(int n) {
        return n * factorial(n - 1);   // base case নেই!
    }

    public static void main(String[] args) {
        System.out.println(factorial(5));
    }
}
Exception in thread "main" java.lang.StackOverflowError
	at Main.factorial(Main.java:3)
	at Main.factorial(Main.java:3)
	at Main.factorial(Main.java:3)
	... (একই লাইন শত শত বার)

n চলে গেল 5, 4, … 0, −1, −2 … প্রতিটি ডাক একটি নতুন frame. Thread-এর stack-এর জায়গা সীমিত (সাধারণত এক মেগাবাইটের কাছাকাছি, JVM ও অপারেটিং সিস্টেমভেদে আলাদা), তাই হাজার হাজার ডাকের পর জায়গা ফুরিয়ে যায় আর JVM ছোড়ে StackOverflowError. Stack trace-এ একই লাইন বারবার দেখলেই বুঝবেন: থামার শর্ত নেই, বা শর্তে কখনো পৌঁছানো যাচ্ছে না।

Recursion না loop? Java-তে সাধারণত loop ভালো। কারণ JVM tail call optimization করে না — প্রতিটি recursive ডাক stack-এ জায়গা নেয়, তাই এক লাখ উপাদানের তালিকা recursion দিয়ে হাঁটলে StackOverflowError আসতে পারে, অথচ loop-এ কোনো সমস্যা নেই। factorial বা যোগফলের মতো “একটার পর একটা” কাজে loop নিন। কিন্তু যেখানে ডেটার আকৃতিই গাছের মতো — ফোল্ডারের ভেতরে ফোল্ডার, family tree, JSON-এর ভেতরে JSON — সেখানে recursion প্রায়ই অনেক বেশি পরিষ্কার। পর্ব ৫৪-এ অ্যালগরিদমে এটা আবার দেখবেন।

৯ · ভালো method — Javadoc, pure method আর একটি কাজ

Javadoc — method-এর ব্যবহারবিধি

Method-এর ঠিক ওপরে /** … */ দিয়ে লেখা comment হলো Javadoc. এতে লেখা হয় method কী করে, প্রতিটি parameter কী (@param), আর কী ফেরত দেয় (@return). IntelliJ বা VS Code-এ method-এর নামের ওপর মাউস রাখলেই এই লেখা ভেসে ওঠে, আর javadoc টুল এখান থেকে HTML ডকুমেন্টেশন বানায় — Java-র নিজের সব API ডকুমেন্টেশন এভাবেই তৈরি।

/**
 * দামের সাথে VAT যোগ করে মোট দাম ফেরত দেয়।
 *
 * @param price VAT ছাড়া দাম, টাকায়; ঋণাত্মক হবে না
 * @param rate  VAT-এর হার, যেমন 0.15 মানে ১৫ শতাংশ
 * @return VAT-সহ মোট দাম
 */
static double addVat(double price, double rate) {
    return price + price * rate;
}

ভালো Javadoc বলে কোড পড়ে যা বোঝা যায় না: একক (টাকা না পয়সা?), সীমা (ঋণাত্মক চলবে?), আর হারটি ১৫ না ০.১৫ আকারে দিতে হবে। “@param price the price” ধরনের লেখা কোনো তথ্য দেয় না — সেটা না লেখাই ভালো।

Pure method বনাম side effect

addVat-এর ফল শুধু তার argument-এর ওপর নির্ভর করে, আর সে বাইরের কিছু বদলায় না, কিছু ছাপেও না। একই input দিলে হাজারবার একই output. এমন method-কে বলে pure method. উল্টো দিকে, যে method স্ক্রিনে ছাপে, ফাইলে লেখে, argument-এর object বদলায় (অংশ ৭-এর addTitle), সে side effect তৈরি করে।

Side effect খারাপ নয় — প্রোগ্রাম কিছু ছাপে না বা সেভ না করলে তার কোনো মূল্যই নেই। কিন্তু হিসাব আর side effect এক method-এ মিশিয়ে ফেললে সমস্যা। calculateAndPrintGpa() পরীক্ষা করতে হলে স্ক্রিনের লেখা ধরতে হয়; calculateGpa() শুধু একটি সংখ্যা ফেরত দিলে এক লাইনে যাচাই করা যায়: ফল কি 3.75? পর্ব ৪৭-এ JUnit দিয়ে ঠিক এভাবেই টেস্ট লিখবেন। নিয়ম: হিসাব করুন pure method-এ, ছাপুন বা সেভ করুন আলাদা, পাতলা method-এ।

কত বড় হবে একটি method?

কোনো নির্দিষ্ট লাইন-সংখ্যা নেই, কিন্তু কয়েকটি সংকেত আছে। Method-এর কাজ এক বাক্যে বলতে গিয়ে যদি “এবং” লাগে (“ইনপুট নেয় এবং হিসাব করে এবং রসিদ ছাপে”), তাহলে সেটা তিনটি method. Method-এর ভেতরে comment দিয়ে ভাগ করা অংশ (// step 2: discount) প্রায়ই বলে দেয় যে সেই অংশটি নিজেই একটি method হতে চায় — আর comment-টিই তার নাম। পাঁচ-ছয়টির বেশি parameter হলে ভাবুন, এগুলো একসাথে কোনো object বা record হওয়া উচিত কি না (পর্ব ২৫)।

পরীক্ষা ও ইন্টারভিউতে
  • Java কি pass-by-value না pass-by-reference? — সবসময় pass-by-value; object-এর ক্ষেত্রে reference-এর মান কপি হয়, তাই object-এর ভেতরটা বদলানো যায় কিন্তু caller-এর ভ্যারিয়েবলকে অন্য object দেখানো যায় না।
  • শুধু return type আলাদা করে overload করা যায়? — না; signature হলো নাম আর parameter টাইপ, return type তার অংশ নয়, তাই “already defined” error.
  • Overloading আর overriding-এর পার্থক্য? — Overloading: একই নাম, ভিন্ন parameter, compile-এর সময় বাছাই। Overriding: subclass-এ একই signature, চলার সময় বাছাই।
  • Varargs-এর নিয়ম? — একটিই varargs parameter, সবার শেষে; ভেতরে সেটি array; overload বাছাইয়ে সবার শেষে বিবেচিত।
  • StackOverflowError কেন হয়? — সাধারণত base case-হীন বা অতিরিক্ত গভীর recursion-এ call stack-এর জায়গা ফুরিয়ে গেলে; এটি compile error নয়, চলার সময়ের Error.

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

AI-কে “একটি বিলিং প্রোগ্রাম লিখে দাও” বললে সে প্রায়ই একটি দুশো লাইনের main দেয়, অথবা চমৎকার দেখতে কিছু method — যার একটির ভেতরে ইনপুট, হিসাব, ছাপা সব মেশানো। কোড চলে, কিন্তু পড়া, বদলানো আর টেস্ট করা কঠিন। আর ধারণাগত প্রশ্নে (“Java-তে object কীভাবে পাস হয়?”) অনেক AI এখনো ইন্টারনেটের পুরোনো, ভুল বাক্যটি আওড়ায়।

AI যা করেকেন সমস্যাআপনি যা করবেন
একটি বিশাল method-এ সব কাজটেস্ট করা যায় না, একটি অংশ বদলালে অন্যটি ভাঙে“প্রতিটি method একটি কাজ করবে, হিসাব pure হবে” — শর্ত দিন
বলে “Java passes objects by reference”এই ভুল ধারণা থেকে swap-এর মতো কাজ-না-করা কোড আসেঅংশ ৭-এর replace উদাহরণ দিয়ে তাকে যাচাই করতে বলুন
সব method আর field staticObject-ভিত্তিক নকশা হারায়, global অবস্থার মতো আচরণ করেশেখার শুরুতে ঠিক আছে; পর্ব ১৫-এর পর থেকে instance method চান
Javadoc নেই, বা “@param x the x”একক, সীমা, null চলবে কি না — কিছুই জানা যায় নাJavadoc-এ একক আর শর্ত লিখতে বলুন
প্রায় একই রকম overload, long আর Integer মিশিয়েকোন ডাক কোথায় যাবে তা অনুমান করা কঠিনআলাদা নাম চান, বা overload কমাতে বলুন

AI-কে method লিখতে বা রিভিউ করতে বলার সময় নিচের কাঠামো ব্যবহার করুন। শর্তগুলোই আসল — এগুলো ছাড়া AI নিজের অভ্যাসে লিখবে:

আমি Java 21 শিখছি। নিচের কাজের জন্য static method লিখে দাও (এখনো class/object শিখিনি)। কাজ: [যেমন — নম্বর থেকে GPA বের করা, VAT যোগ করা, টাকাকে ডলারে বদলানো] শর্ত: ১. প্রতিটি method একটিই কাজ করবে; নাম হবে ক্রিয়াপদ, camelCase. ২. হিসাবের method pure হবে — কিছু ছাপবে না, input নেবে না; ছাপার কাজ main-এ। ৩. প্রতিটি method-এ Javadoc দাও: @param-এ একক ও সীমা, @return-এ কী ফেরত আসে। ৪. সব পথে return আছে কি না নিশ্চিত করো; ভুল input (যেমন ঋণাত্মক দাম) হলে কী হবে তা বলো। ৫. কোনো method যদি argument-এর object বদলায়, আলাদা করে উল্লেখ করো। ৬. শেষে এক লাইনে বলো: Java-তে argument কীভাবে পাস হয় — আর আমার কোডে তার কোনো প্রভাব আছে কি না।

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

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

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

প্র ০১“Java is pass-by-value” — এই বাক্য নিয়ে বছরের পর বছর তর্ক কেন চলে? Java-র নিজের নিয়মপুস্তক কী বলে?

তর্কটা আসলে শব্দ নিয়ে, আচরণ নিয়ে নয়। আচরণ নিয়ে কারও দ্বিমত নেই: method-এর ভেতরে object-এর ভেতরটা বদলালে caller দেখে, parameter-এ নতুন object বসালে দেখে না। প্রশ্ন হলো একে কী নামে ডাকব।

“Pass by reference” শব্দের আসল অর্থ। প্রোগ্রামিং ভাষার তত্ত্বে pass-by-reference মানে parameter হয় caller-এর ভ্যারিয়েবলেরই আরেকটি নাম (alias). Parameter-এ নতুন মান বসালে caller-এর ভ্যারিয়েবলেও সেটা বসে। C++-এর int&, C#-এর ref, Pascal-এর var parameter এর উদাহরণ। এই সংজ্ঞায় পরীক্ষা খুব সহজ: একটি কাজ করা swap লেখা যায় কি? Java-তে যায় না — তাই Java pass-by-reference নয়।

Java Language Specification কী বলে। JLS-এর §8.4.1 বলে, method ডাকার সময় argument expression-গুলোর মান দিয়ে নতুন তৈরি parameter ভ্যারিয়েবলগুলোকে initialize করা হয়। আর §4.3.1 বলে, reference value আসলে object-এর প্রতি pointer, অথবা একটি বিশেষ null reference. দুটি একসাথে পড়লে নিয়মটি পরিষ্কার: ভ্যারিয়েবলের মান হলো reference, আর সেই মানটিই কপি হয়। Java-র স্রষ্টা James Gosling সহ-লেখক হিসেবে যে বই লিখেছেন (The Java Programming Language), সেখানেও স্পষ্ট বলা আছে যে Java-তে parameter পাঠানোর একটিই পদ্ধতি — pass by value.

তাহলে ভুলটা এত ছড়াল কেন? কারণ “reference” শব্দটি দুই জায়গায় দুই অর্থে বসে। Java object-এর ঠিকানাকে বলে reference; আর C++-এ reference মানে alias. “Java passes references” কথাটি সত্য, কিন্তু “Java passes by reference” কথাটি ভুল — একটি ছোট “by” পুরো অর্থ বদলে দেয়। অনেক প্রাচীন টিউটোরিয়াল, এমনকি কিছু বইও এই দুটো মিশিয়েছে, আর AI সেই লেখাগুলো থেকেই শিখেছে। কেউ কেউ এই আচরণের জন্য তৃতীয় একটি নাম ব্যবহার করেন — “call by sharing”, যা Barbara Liskov-এর দল ১৯৭০-এর দশকে CLU ভাষার বর্ণনায় চালু করেছিল; Python-এর আচরণও একই রকম।

কেন এটা শুধু শব্দের খেলা নয়। যিনি ভাবেন “object by reference”, তিনি replace বা swap-এর মতো কোড লিখে অবাক হন, অথবা ভাবেন method-এ পাঠানো list “নিরাপদ কপি” — দুটিই বাস্তব bug-এর উৎস। সঠিক মডেল (“ঠিকানার কপি”) দুটি ভুলই আটকায়।

মূল উপলব্ধি: Java সবসময় মান কপি করে; object-এর ক্ষেত্রে সেই মান একটি ঠিকানা। “ভেতরটা বদলানো যায়, তীর ঘোরানো যায় না” — এই এক বাক্য মনে রাখলে তর্কটাই অর্থহীন হয়ে যায়।

প্র ০২C++, Python, Kotlin — সবাই default parameter মান দেয়। Java দেয় না কেন? বদলে কী ব্যবহার করা হয়?

Python-এ def add_vat(price, rate=0.15) লেখা যায়, C++-এ double addVat(double price, double rate = 0.15). Java-তে এমন কিছু নেই, আর প্রায় তিন দশকে যোগও হয়নি। এটা ভুলে যাওয়া নয়, একটি সচেতন নকশা-সিদ্ধান্ত, যার পেছনে কয়েকটি যুক্তি আছে।

১. Overloading দিয়েই একই কাজ হয়। এই পর্বের addVat(price) যা করে — দ্বিতীয় সংস্করণকে 0.15 দিয়ে ডাকা — সেটাই default parameter-এর কাজ। Java-র শুরুর নকশাকারেরা ভাষাকে ছোট রাখতে চেয়েছিলেন; একই সমস্যার দুটি সমাধান থাকলে একটি রাখো — এটাই ছিল দর্শন। C++-এ default argument আর overloading একসাথে থাকায় যে ambiguity তৈরি হয়, Java সেটা এড়িয়েছে।

২. Binary compatibility-র সমস্যা। C++-এ default মান বসানো হয় ডাকার জায়গায়, compile-এর সময়। Library-র লেখক default 0.15 থেকে 0.10 করলে, যারা পুরোনো library দিয়ে compile করেছে তাদের কোডে 0.15-ই থেকে যায় — যতক্ষণ না তারা নতুন করে compile করে। Java-র জগতে আলাদাভাবে compile করা jar ফাইল একসাথে চলে, library আপডেট হয় caller-কে recompile না করেই। Overload-এ default মান থাকে library-র ভেতরে, তাই library বদলালেই সবাই নতুন মান পায়।

৩. অনেক parameter হলে — builder. Overloading-এর দুর্বলতা হলো parameter বাড়লে overload-এর সংখ্যা লাফিয়ে বাড়ে — একে বলে “telescoping” constructor/method. Joshua Bloch তাঁর Effective Java বইয়ে এর সমাধান হিসেবে builder pattern প্রস্তাব করেছেন: Order.builder().item("চা").qty(2).discount(0.1).build() — যা দরকার শুধু সেটুকু নাম ধরে বলুন, বাকিগুলো default. পর্ব ৪৮-এ design pattern-এ এটা দেখবেন।

অন্যরা কী করে। Kotlin JVM-এই চলে এবং default parameter দেয়; Java থেকে সেই method ডাকার সুবিধার জন্য @JvmOverloads annotation দিলে Kotlin compiler নিজেই Java-র জন্য overload-গুলো তৈরি করে দেয়। অর্থাৎ JVM স্তরে শেষ পর্যন্ত সেই overloading-ই।

মূল উপলব্ধি: একটি বৈশিষ্ট্যের অনুপস্থিতিও নকশা। Java সুবিধার চেয়ে সরলতা আর library আপডেটের নিরাপত্তাকে বেশি গুরুত্ব দিয়েছে — আর ঘাটতি পূরণ করেছে overloading ও builder দিয়ে।

প্র ০৩Varargs Java-তে এলো Java 5-এ। তার আগে মানুষ কী করত, আর varargs আসলে ভেতরে কী?

২০০৪ সালে Java 5 ভাষায় একসাথে অনেকগুলো বড় পরিবর্তন আনে — generics, enum, autoboxing, for-each loop, annotation আর varargs. এগুলোর অনেকটা একে অপরের ওপর নির্ভরশীল ছিল, আর varargs-এর একটি বড় চাহিদা এসেছিল একটি নতুন method থেকে: printf.

আগের যুগ। Java 5-এর আগে যত খুশি argument নিতে চাইলে method-কে একটি array নিতে হতো, আর ডাকার সময় নিজে array বানাতে হতো। যেমন MessageFormat.format("{0} টাকা, {1} জন", new Object[] { price, count }). প্রতিটি ডাকে new Object[] { … } লেখা ছিল ঝামেলার, আর int-এর মতো primitive হলে আগে হাতে new Integer(price) বানিয়ে নিতে হতো — কারণ autoboxing তখনো ছিল না। C-র printf-এর মতো সহজ কিছু Java-তে ছিল না।

Varargs আসলে syntactic sugar. double... prices লিখলে compiled bytecode-এ সেটা একটি সাধারণ double[] parameter, সাথে একটি flag যে এটি varargs. ডাকার জায়গায় compiler নিজে array বানানোর কোড বসিয়ে দেয়। তাই পুরোনো array-নেওয়া method-গুলো সহজেই varargs-এ রূপান্তর করা গেছে — Arrays.asList আর MessageFormat.format Java 5-এ ঠিক এভাবেই বদলেছিল, পুরোনো কোড না ভেঙে।

খরচ আর সূক্ষ্মতা। যেহেতু প্রতিটি ডাকে একটি নতুন array তৈরি হয়, খুব বেশি ডাকা কোডে এটা অল্প খরচ বাড়ায়। এ কারণেই Java 9-এর List.of-এ শূন্য থেকে দশটি element-এর জন্য আলাদা আলাদা overload আছে, আর তার বেশি হলে varargs সংস্করণ। আরেকটি সূক্ষ্মতা generics-এর সাথে: generic টাইপের varargs (T... items) টাইপ-নিরাপত্তায় একটি ফাঁক তৈরি করতে পারে, যাকে বলে heap pollution — Java 7-এ এর জন্য @SafeVarargs annotation আনা হয়। পর্ব ২৮-এ generics শেখার সময় এর কারণ বুঝবেন।

Overload-এ সবার শেষে কেন? Java 5-এর আগের কোড যেন নতুন নিয়মে অন্য method-এ চলে না যায়। তাই নতুন দুটি সুবিধা — boxing আর varargs — বাছাইয়ের সিঁড়িতে সবচেয়ে নিচে বসানো হয়েছে।

মূল উপলব্ধি: ভালো ভাষা-বৈশিষ্ট্য প্রায়ই একটি বাস্তব যন্ত্রণা থেকে আসে, আর পুরোনো কোড না ভাঙার শর্তে তার নকশা ঠিক হয়। Varargs হলো array-র ওপর একটি সুন্দর মোড়ক — মোড়ক খুললে array-ই।

প্র ০৪অনেক functional ভাষা গভীর recursion নির্ভয়ে চালায়। JVM কেন tail call optimization করে না, আর Java প্রোগ্রামাররা কী করেন?

Tail call কী। যদি একটি method-এর একদম শেষ কাজটি হয় আরেকটি ডাক — return loop(n - 1, acc * n); — তাহলে সেই ডাকের পরে বর্তমান frame-এ আর কিছু করার নেই। চতুর runtime তখন নতুন frame না বানিয়ে পুরোনোটাই আবার ব্যবহার করতে পারে। ফলে recursion চলে loop-এর মতো, stack বাড়ে না। Scheme ভাষার মানদণ্ড এটি বাধ্যতামূলক করেছে, Haskell, OCaml, Erlang-এ এটা প্রতিদিনের ভরসা। খেয়াল করুন, এই পর্বের n * factorial(n - 1) tail call নয় — ডাক শেষে গুণ বাকি থাকে।

JVM কেন করে না। কয়েকটি কারণ প্রায়ই বলা হয়। প্রথমত, পুরোনো Java-র নিরাপত্তা-ব্যবস্থা (Security Manager) কোনো কাজের অনুমতি দেওয়ার আগে call stack হেঁটে দেখত কে কাকে ডেকেছে — frame উধাও করে দিলে সেই হিসাব গোলমাল হতো। দ্বিতীয়ত, stack trace: exception হলে প্রতিটি ডাকের লাইন দেখা Java-র debugging-এর বড় শক্তি, আর tail call বাদ দিলে trace থেকে ফ্রেম হারায়। তৃতীয়ত, Java-র অগ্রাধিকার ছিল সাধারণ object-ভিত্তিক কোডের গতি; ঠিকমতো tail call যোগ করা মানে bytecode আর JVM-এর অনেক গভীরে পরিবর্তন। বছরের পর বছর OpenJDK-তে এ নিয়ে প্রস্তাব আর আলোচনা হয়েছে, কিন্তু Java 21 পর্যন্ত JVM স্বয়ংক্রিয় tail call elimination দেয় না।

বাস্তবে কী হয়। প্রতিটি thread-এর stack-এর আকার সীমিত, যা -Xss option দিয়ে বদলানো যায়। সরল recursion কয়েক হাজার থেকে কয়েক দশ হাজার স্তরে পৌঁছালেই StackOverflowError আসতে পারে — সংখ্যাটা নির্ভর করে frame কত বড় আর JIT কতটা optimize করেছে তার ওপর। তাই Java প্রোগ্রামাররা রৈখিক কাজ (যোগফল, factorial, তালিকা হাঁটা) loop দিয়ে লেখেন, আর গভীর গাছ বা graph-এর জন্য নিজস্ব stack (যেমন ArrayDeque, পর্ব ৩২) ব্যবহার করে recursion-কে loop-এ বদলান।

JVM-এর অন্য ভাষাগুলো। Scala-র @tailrec আর Kotlin-এর tailrec keyword compiler-কে বলে, নিজেকে ডাকা tail recursion-কে compile-এর সময়েই একটি loop-এ বদলে দাও। অর্থাৎ JVM না করলেও compiler কাজটা করে দেয় — তবে শুধু নিজেকে ডাকার ক্ষেত্রে, দুটি method পরস্পরকে ডাকলে নয়।

মূল উপলব্ধি: একটি runtime কী optimize করবে, তা তার অগ্রাধিকারের প্রতিফলন। Java বেছে নিয়েছে পূর্ণ stack trace আর সরল নিরাপত্তা-মডেল; বিনিময়ে Java প্রোগ্রামারকে জানতে হয় কখন recursion ছেড়ে loop ধরতে হবে।

প্র ০৫Robert C. Martin বলেন “function একটিই কাজ করবে, আর হবে ছোট।” এই উপদেশ কি সবসময় ঠিক? বেশি ভাগ করলে কী সমস্যা?

Robert C. Martin (“Uncle Bob”) ২০০৮ সালে প্রকাশিত Clean Code বইয়ের function অধ্যায়ে লিখেছিলেন, function-এর প্রথম নিয়ম হলো সেটা ছোট হবে, আর দ্বিতীয় নিয়ম হলো সেটা আরও ছোট হবে; এবং function একটি কাজ করবে, ভালোভাবে করবে, শুধু সেটাই করবে। বইটি Java উদাহরণে লেখা, আর এক প্রজন্মের Java প্রোগ্রামারের কোড-রুচি গড়েছে।

উপদেশটি কেন কাজের। শত শত লাইনের method — যাকে কেউ কেউ “god method” বলেন — বাস্তবে প্রায়ই দেখা যায়: ইনপুট পড়া, যাচাই, হিসাব, ডেটাবেসে লেখা, ইমেইল পাঠানো এক জায়গায়। এমন method টেস্ট করা প্রায় অসম্ভব, একটি অংশ বদলালে অন্য অংশে bug ঢোকে, আর নতুন কেউ বুঝতে দিন পার করে। ভালো নামের ছোট method সেই জটকে আলাদা আলাদা নামের পেছনে ভাগ করে দেয় — validateOrder, calculateTotal, sendReceipt. ওপরের স্তরের method তখন প্রায় বাংলা বাক্যের মতো পড়া যায়।

অতিরিক্ত ভাগ করার বিপদ। সমালোচকেরা বলেন, নিয়মটি আক্ষরিক মানলে উল্টো সমস্যা হয়: তিন-চার লাইনের অসংখ্য method, যারা একে অপরকে ডাকে, আর একটি সাধারণ কাজ বুঝতে দশটি method লাফিয়ে লাফিয়ে পড়তে হয়। John Ousterhout তাঁর A Philosophy of Software Design (২০১৮) বইয়ে এর বিপরীতে “deep module”-এর ধারণা দিয়েছেন — সরল interface-এর পেছনে যথেষ্ট কাজ লুকানো থাকবে; যে method নিজের নামের চেয়ে বেশি কিছু করে না, সে আসলে জটিলতা কমায় না, বরং interface-এর সংখ্যা বাড়ায়। “একটি কাজ” নিজেও আপেক্ষিক — processOrder একটি কাজ, আবার তার ভেতরের প্রতিটি ধাপও একটি কাজ।

ব্যবহারিক ভারসাম্য। ভাগ করুন যখন অংশটির একটি অর্থপূর্ণ নাম দেওয়া যায়, যখন সেটা আলাদাভাবে টেস্ট করার মতো, বা একাধিক জায়গায় লাগে। ভাগ করবেন না শুধু লাইন গোনার জন্য, যদি নতুন method-এর নাম ভেতরের কোডের চেয়ে বেশি কিছু না বলে। আর মনে রাখুন, এই পর্বের pure method-এর নিয়মটি দুই পক্ষই সমর্থন করে: হিসাব আর side effect আলাদা রাখা।

মূল উপলব্ধি: লক্ষ্য ছোট method নয়, বোঝা সহজ কোড। ছোট, ভালো নামের method সাধারণত সেখানে পৌঁছায় — কিন্তু যখন ভাগ করা পড়ার পথ লম্বা করে, তখন বিচারবুদ্ধিই নিয়মের চেয়ে বড়।

অনুশীলন

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

  1. আউটপুট অনুমান করুন: অংশ ৭-এর প্রোগ্রামে main-এ দুটি লাইনের ক্রম উল্টে দিন — আগে replace(name);, তারপর addTitle(name);. না চালিয়ে বলুন name কী ছাপবে, তারপর চালিয়ে মিলিয়ে নিন।

    এখনো Dr. Rahima. replace শুধু নিজের parameter-এ নতুন object বসায়, main-এর name তখনো আসল “Rahima” object দেখায়। তার পরে addTitle সেই আসল object-এর ভেতরে “Dr. ” বসায়। ক্রম বদলালেও ফল একই, কারণ replace-এর কোনো প্রভাব কখনোই caller-এর কাছে পৌঁছায় না। নতুন “Keu na” object-টির দিকে method শেষে আর কেউ তাকিয়ে নেই, তাই garbage collector পরে তাকে সরিয়ে দেবে (পর্ব ০৫)।

  2. Missing return ঠিক করুন: অংশ ২-এর gradePoint method-টি এমনভাবে ঠিক করুন যাতে compile হয় এবং ৫০ থেকে ৫৯ পেলে 2.0 আর ৫০-এর কম পেলে 0.0 ফেরত দেয়। তারপর ভাবুন: ১২০ বা −৫ নম্বর এলে কী হওয়া উচিত?
    static double gradePoint(int marks) {
        if (marks >= 80) return 4.0;
        if (marks >= 70) return 3.5;
        if (marks >= 60) return 3.0;
        if (marks >= 50) return 2.0;
        return 0.0;                 // বাকি সব পথ
    }

    শেষের শর্তহীন return 0.0; compiler-কে নিশ্চিত করে যে প্রতিটি পথে একটি মান ফেরত যায়। প্রতিটি if-এ return থাকায় else লাগে না। ১২০ বা −৫ আসলে ভুল input — এখন ১২০ পেলে 4.0 আর −৫ পেলে 0.0 আসবে, যা নীরবে ভুল। ভালো সমাধান হলো শুরুতেই যাচাই করে exception ছোড়া (পর্ব ২৯), আর Javadoc-এ লিখে রাখা যে marks ০ থেকে ১০০-এর মধ্যে হতে হবে।

  3. Overload যোগ করুন: অংশ ৪-এর প্রোগ্রামে takaToDollar-এর একটি overload লিখুন, যেটি বিনিময় হারও parameter হিসেবে নেয়। পুরোনো একটি-parameter সংস্করণটি যেন নতুনটিকে ডাকে। তারপর takaToDollar(1200, 110) ডেকে দেখুন।
    static double takaToDollar(double taka) {
        return takaToDollar(taka, 120.0);
    }
    
    static double takaToDollar(double taka, double rate) {
        return taka / rate;
    }

    takaToDollar(1200, 110) ছাপবে প্রায় 10.91 (%.2f দিয়ে)। খেয়াল করুন, 110 একটি int, কিন্তু widening দিয়ে double হয়ে দুই-parameter সংস্করণে বসেছে। একটি-parameter সংস্করণ এখন শুধু ডিফল্ট হার দিয়ে দ্বিতীয়টিকে ডাকে — সূত্র থাকল এক জায়গায়। Java-তে default parameter-এর কাজ এভাবেই হয় (ভাবনার প্রশ্ন ২)।

  4. Stack হাতে আঁকুন: sum(addVat(100), addVat(200, 0.05)) — এই একটি লাইন চলার সময় call stack-এ কোন কোন frame কোন ক্রমে আসে আর যায়, কাগজে লিখুন। কোন method সবচেয়ে আগে শেষ হয়, আর sum কোন মানগুলো পায়?

    Java argument বাঁ থেকে ডানে হিসাব করে, আর sum ডাকার আগে তার সব argument-এর মান জানা থাকতে হয়। ক্রম: (১) addVat(100)-এর frame আসে, সে ভেতরে addVat(100, 0.15) ডাকে — দুটি frame একসাথে; ভেতরেরটি 115.0 ফেরত দিয়ে চলে যায়, তারপর বাইরেরটি। (২) addVat(200, 0.05)-এর frame আসে, 210.0 ফেরত দিয়ে যায়। (৩) এবার sum-এর frame, যার prices হলো {115.0, 210.0}; সে 325.0 ফেরত দেয়। সবার আগে শেষ হয় addVat(100, 0.15) — যে সবচেয়ে ভেতরে আর সবচেয়ে শেষে এসেছিল। sum শুরুই হয় সবার শেষে।

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

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

ক্রমJava-তে একটি method ডাকা থেকে ফল ফেরত পাওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧮 ডাকার জায়গায় argument-গুলোর মান বাঁ থেকে ডানে হিসাব হয়
  2. 📥 call stack-এ নতুন frame তৈরি হয়, মানগুলো কপি হয়ে parameter-এ বসে
  3. ⚙️ Method-এর শরীর চলে, local variable তৈরি হয়
  4. ↩️ return মান ফেরত পাঠায়, frame মুছে যায়
  5. ▶️ Caller থেমে থাকা জায়গা থেকে ফেরত মান নিয়ে আবার চলে

static void inc(int n) { n++; } — main-এ int a = 5; inc(a); System.out.println(a); লিখলে কী ছাপবে?

  • 5
  • 6
  • compile error
  • 0

একই class-এ নিচের কোন জোড়া method একসাথে রাখলে compile error হবে?

  • double area(double r) আর double area(double w, double h)
  • int max(int a, int b) আর double max(double a, double b)
  • int parse(String s) আর double parse(String s)
  • void log(String m, int level) আর void log(int level, String m)

একটি class-এ শুধু static void print(long x) আর static void print(Integer x) আছে। print(5) ডাকলে কোনটি চলবে?

  • print(Integer)
  • print(long)
  • compile error: ambiguous
  • যেকোনোটি, JVM চলার সময় ঠিক করে

Java-তে varargs parameter-সহ নিচের কোন method declaration বৈধ?

  • void f(int... nums, String label)
  • void f(int... a, int... b)
  • void f(int nums...)
  • void f(String label, int... nums)

একটি recursive method-এ base case নেই, প্রতিবার নিজেকে n - 1 দিয়ে ডাকে। কী ঘটবে?

  • javac compile error দেবে
  • চলার সময় StackOverflowError আসবে
  • প্রোগ্রাম চিরকাল চলতে থাকবে, কোনো error ছাড়া
  • n শূন্যে পৌঁছালে JVM নিজেই থামিয়ে 0 ফেরত দেবে

Java-তে একটি method-এর signature কী দিয়ে গঠিত?

  • নাম আর return type
  • নাম আর parameter-গুলোর নাম
  • নাম আর parameter-গুলোর টাইপের ক্রম
  • modifier, return type আর নাম

শব্দকোষ

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

মনে রাখুন

  1. Method = নাম + parameter + return type + শরীর; signature হলো নাম আর parameter টাইপ, return type নয়। নাম ক্রিয়াপদে, camelCase-এ।
  2. প্রতিটি পথে return চাই, নইলে missing return statement; local variable শুধু নিজের block-এ বাঁচে, প্রতিটি ডাক call stack-এ নিজের frame পায়।
  3. Overload বাছাই তিন ধাপে — widening, তারপর boxing, তারপর varargs; varargs একটিই, সবার শেষে, ভেতরে array.
  4. Java সবসময় pass-by-value — object-এর ক্ষেত্রে ঠিকানার কপি: ভেতরটা বদলানো যায়, parameter-এ নতুন object বসালে caller টের পায় না; swap লেখা যায় না।
  5. Recursion-এ base case অপরিহার্য, নইলে StackOverflowError; Java-তে রৈখিক কাজে loop ভালো। হিসাব রাখুন pure method-এ, আর Javadoc-এ একক ও সীমা লিখুন।

পরের পর্বে: varargs-এর ভেতরে যে জিনিসটি লুকিয়ে ছিল — array. একই ধরনের অনেক মান এক নামে রাখা, index দিয়ে পড়া, Arrays class-এর সুবিধা, আর method-এ array পাঠালে কেন ভেতরের মান বদলে যায় — এই পর্বের pass-by-value সেখানে আবার কাজে লাগবে।

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

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