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

exception — checked, unchecked আর try-with-resources

Exceptions — the Throwable Hierarchy, Stack Traces, try/catch/finally, Checked vs Unchecked, Custom Exceptions and try-with-resources

Taught in Bangla, this lesson covers exceptions: how a thrown exception unwinds the call stack, the Throwable, Error, Exception and RuntimeException hierarchy, reading stack traces, try, catch and finally with catch ordering and multi-catch, throw versus throws, checked versus unchecked, custom exceptions and chaining, and try-with-resources with suppressed exceptions.

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

  • exception আসলে কী, আর stack unwinding — কীভাবে সে method থেকে method-এ ওপরে ওঠে
  • Throwable → Error / Exception → RuntimeException গাছ, আর stack trace লাইন ধরে পড়া
  • try/catch/finally, একাধিক catch-এর ক্রম, multi-catch, throw বনাম throws
  • checked বনাম unchecked, নিজের exception বানানো, chaining, আর কোন অভ্যাসগুলো exception-কে নষ্ট করে
  • try-with-resources, AutoCloseable, বন্ধ হওয়ার উল্টো ক্রম আর suppressed exception

১ · exception কী — “আর এগোতে পারছি না” বার্তা

একটি মোবাইল ওয়ালেট অ্যাপ ভাবুন। ব্যবহারকারী ৮০০ টাকা পাঠাতে চাইলেন, কিন্তু ব্যালেন্স আছে ৫০০. টাকা তোলার method withdraw() এখন কী করবে? চুপচাপ ব্যালেন্স ঋণাত্মক করে দেবে? সেটা সবচেয়ে খারাপ। শূন্য বা -1 ফেরত দেবে? তাহলে যে method তাকে ডেকেছে, তাকে প্রতিবার মনে করে ফেরত-মান যাচাই করতে হবে — আর একবার ভুলে গেলেই ভুল নীরবে ছড়িয়ে পড়বে। C ভাষায় ঠিক এভাবেই কাজ হয়, আর C-এর অনেক বড় bug এই “ফেরত-মান যাচাই করতে ভুলে যাওয়া” থেকেই আসে।

Java অন্য পথ নেয়। withdraw() নিজে জানে না এই অবস্থায় কী করা উচিত — ব্যবহারকারীকে বার্তা দেখানো, অন্য অ্যাকাউন্ট থেকে নেওয়া, নাকি লেনদেন বাতিল — সেটা ঠিক করবে ওপরের কোনো কোড। তাই সে একটি exception ছুড়ে দেয়: একটি object, যার ভেতরে লেখা থাকে কী ভুল হলো (টাইপ আর বার্তা) আর কোথায় হলো (stack trace). ছোড়ামাত্র method-এর বাকি কোড আর চলে না।

একটি গার্মেন্টস কারখানার লাইন ভাবুন। একজন অপারেটর দেখলেন কাপড়ের রোলে ছেঁড়া অংশ। তিনি নিজে ঠিক করতে পারবেন না, আবার চুপচাপ সেলাই চালিয়ে গেলে নষ্ট শার্ট বেরোবে। তাই তিনি লাল বাতি জ্বালিয়ে সুপারভাইজারকে খবর দেন। সুপারভাইজার না পারলে খবর যায় ফ্লোর ম্যানেজারের কাছে — যতক্ষণ না এমন কেউ পাওয়া যায় যিনি জানেন কী করতে হবে। মাঝের কেউ যদি সমাধান জানেন, খবর সেখানেই থামে। আর কেউ না জানলে পুরো লাইন বন্ধ। exception ঠিক এই লাল বাতি।

exception ছোড়া হলে JVM একটি নির্দিষ্ট কাজ করে: যে method-এ exception ঘটেছে, সেখানে মিলে যাওয়া catch আছে কি না দেখে। না থাকলে সেই method-এর frame call stack থেকে সরিয়ে দেয় আর তাকে যে ডেকেছিল তার কাছে যায়। এভাবে একটির পর একটি frame সরানোকে বলে stack unwinding. কেউ না ধরলে main-ও সরে যায়, প্রোগ্রাম থেমে যায়, আর standard error-এ ছাপা হয় একটি stack trace. তিনটি method-এর একটি ছোট শিকলে ব্যাপারটা দেখুন:

1void withdraw(long amt) throws InsufficientBalanceException {2    if (amt > balance)3        throw new InsufficientBalanceException(...);4    balance -= amt;5}6void transfer(Wallet w, long amt) throws InsufficientBalanceException {7    w.withdraw(amt);8    System.out.println("পাঠানো হলো");9}10public static void main(String[] args) {11    try {12        transfer(wallet, 800);13    } catch (InsufficientBalanceException e) {14        System.out.println("ব্যর্থ: " + e.getMessage());15    }16    System.out.println("অ্যাপ চলছে");17}
call stack আর exception
stack (নিচ → ওপর)
exception
balance
  1. main ডাকে transfer. stack-এ এখন দুটি frame. try block-এর ভেতর থেকে ডাকা হয়েছে — অর্থাৎ main আগেই ঘোষণা দিয়ে রেখেছে: “এর ভেতরে InsufficientBalanceException এলে আমি সামলাব।”
  2. transfer ডাকে withdraw. তিনটি frame. transfer-এর নিজের কোনো try নেই; তার ঘোষণায় শুধু throws লেখা — “এই exception আমার ভেতর দিয়ে বাইরে যেতে পারে।”
  3. throw. ৮০০ > ৫০০, তাই একটি exception object তৈরি হলো। তৈরির মুহূর্তেই সে রেকর্ড করে নেয় এখন stack-এ কে কে আছে — পরে এটাই stack trace হয়ে ছাপা হবে।
  4. withdraw-এর frame সরে গেল। এখানে কোনো catch নেই, তাই লাইন ৪ (balance -= amt) আর চলল না। ব্যালেন্স অক্ষত — exception-এর এটাই বড় সুবিধা: ভুল অবস্থায় কোড এগোয় না।
  5. transfer-এর frame-ও সরল। “পাঠানো হলো” ছাপা হলো না — সেটা হলে ব্যবহারকারী মিথ্যা খবর পেতেন। মাঝের method-কে কোনো if লিখতে হয়নি; exception নিজেই তার ওপর দিয়ে চলে গেল।
  6. main-এর catch মিলে গেল। exception-এর টাইপ catch-এর টাইপের সাথে মেলে (একই class বা তার subclass), তাই unwinding থামল আর catch block চলল।
  7. প্রোগ্রাম স্বাভাবিকভাবে চলছে। catch শেষ হলে চলা শুরু হয় পুরো try/catch-এর পরের লাইন থেকে — ফিরে withdraw-এ যাওয়া হয় না। Java-র exception “resume” করে না, “terminate” করে।
মূল ধারণা

exception আলাদা করে দুটি দায়িত্ব: ভুল ধরা (যেখানে ঘটে — withdraw) আর ভুল সামলানো (যে জানে কী করতে হবে — main বা UI). মাঝের সব method পরিষ্কার থাকে, প্রতিটি লাইনের পর if (error) লাগে না।

২ · পরিবারের গাছ — Throwable, Error, Exception, RuntimeException

Java-তে যেকোনো object ছোড়া যায় না। ছোড়া যায় শুধু Throwable class-এর বংশধরদের। এই গাছটি পুরো পর্বের মানচিত্র, তাই একবার ভালো করে দেখে নিন:

Throwable-এর পরিবার

The java.lang.Throwable hierarchy — what is checked and what is not

Throwable
├── Error                         ← unchecked · JVM-এর গুরুতর সমস্যা
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception                     ← checked (RuntimeException বাদে)
    ├── IOException
    │   └── FileNotFoundException
    ├── InsufficientBalanceException   (নিজের বানানো, checked)
    └── RuntimeException          ← unchecked · সাধারণত প্রোগ্রামের ভুল
        ├── NullPointerException
        ├── ArithmeticException
        ├── IllegalArgumentException
        │   └── NumberFormatException
        ├── IllegalStateException
        ├── IndexOutOfBoundsException
        │   └── ArrayIndexOutOfBoundsException
        └── ClassCastException
ছবি ২৮.১ — RuntimeException আর Error-এর নিচের সব unchecked; Exception-এর বাকি সব checked.

Error

JVM নিজেই বিপদে: মেমরি শেষ (OutOfMemoryError), অসীম recursion-এ stack শেষ (StackOverflowError, পর্ব ১২)। সাধারণ অ্যাপের কোড এগুলো ধরে কিছু করতে পারে না।

“ধরবেন না”

checked Exception

বাইরের জগতের ব্যর্থতা, যা ঠিকঠাক কোডেও ঘটতে পারে: ফাইল নেই, নেটওয়ার্ক কেটে গেল, ব্যালেন্স কম। checked exception সামলানো বা ঘোষণা করা বাধ্যতামূলক।

“ঘটতে পারে — পরিকল্পনা রাখুন”

RuntimeException

প্রায় সবসময় প্রোগ্রামারের ভুল: null-এর ওপর method, সীমার বাইরের index, ভুল argument. unchecked exception — compiler কিছু বলে না।

“ঘটা উচিত নয় — কোড ঠিক করুন”

Error শাখার নামগুলো ...Error দিয়ে শেষ হয়, আর Exception শাখারগুলো ...Exception দিয়ে — এই নামকরণই অর্ধেক কাজ করে দেয়। মনে রাখার সহজ নিয়ম: RuntimeException বা Error-এর নিচে হলে unchecked, Exception-এর নিচে বাকি সব checked. এই ভাগ শুধু compiler-এর নিয়ম; চলার সময় দুই ধরনের exception হুবহু একইভাবে ছোড়া, ধরা আর unwind হয়।

“checked exception” Java-র নিজস্ব পরীক্ষা। ১৯৭০-এর দশকে MIT-তে Barbara Liskov-এর CLU ভাষায় আর পরে Modula-3-তে procedure-এর ঘোষণায় কোন exception বেরোতে পারে তা লেখার রীতি ছিল। ১৯৯৫ সালে Java সেই ধারণাকে compile-এর সময়ের কঠোর নিয়মে পরিণত করে, আর মূলধারার ভাষার মধ্যে Java-ই একমাত্র যেখানে এটি এত বড় পরিসরে টিকে আছে। পরের C#, Kotlin, Scala ইচ্ছে করেই এটি নেয়নি — কেন, সেই বিতর্ক ভাবনার প্রশ্নে।

৩ · stack trace পড়া — ওপর থেকে নিচে, “Caused by” পর্যন্ত

নতুনরা stack trace দেখলে ভয় পান — লাল লাল লেখা, অনেক লাইন। কিন্তু এটি আসলে একটি সুন্দর গোয়েন্দা-রিপোর্ট। নিচের প্রোগ্রামে withdraw একটি checked exception ছোড়ে, transfer সেটা ধরে আরেকটি exception-এ মুড়ে আবার ছোড়ে (এটাকে বলে chaining, অংশ ৭-এ বিস্তারিত), আর কেউ শেষটা ধরে না:

class InsufficientBalanceException extends Exception {
    InsufficientBalanceException(String msg) { super(msg); }
}
class Wallet {
    private long balance;
    Wallet(long balance) { this.balance = balance; }
    void withdraw(long amount) throws InsufficientBalanceException {
        if (amount > balance)
            throw new InsufficientBalanceException("balance " + balance + ", requested " + amount);
        balance -= amount;
    }
}
class Main {
    static void transfer(Wallet from, long amount) {
        try {
            from.withdraw(amount);
        } catch (InsufficientBalanceException e) {
            throw new IllegalStateException("transfer failed", e);
        }
    }
    public static void main(String[] args) {
        transfer(new Wallet(500), 800);
    }
}
Exception in thread "main" java.lang.IllegalStateException: transfer failed
	at Main.transfer(Main.java:18)
	at Main.main(Main.java:22)
Caused by: InsufficientBalanceException: balance 500, requested 800
	at Wallet.withdraw(Main.java:9)
	at Main.transfer(Main.java:16)
	... 1 more

এভাবে পড়ুন:

  • প্রথম লাইন — কী আর কোন thread-এ: main thread-এ একটি java.lang.IllegalStateException, বার্তা transfer failed. নিজের বানানো class-এর আগে package নেই, তাই InsufficientBalanceException শুধু নামেই দেখায়।
  • at লাইনগুলো — সবচেয়ে ওপরেরটি যেখানে exception তৈরি হয়েছে: Main.transfer, ১৮ নম্বর লাইন। তার নিচে যে তাকে ডেকেছে — main, ২২ নম্বর লাইন। অর্থাৎ ওপরের লাইন = সবচেয়ে নতুন frame, ঠিক stack-এর মতো।
  • Caused by: — আসল কারণ: ভেতরে মোড়ানো exception. এখানেই আসল গল্প: Wallet.withdraw-এর ৯ নম্বর লাইনে ব্যালেন্স কম পাওয়া গেছে। লম্বা trace-এ সবচেয়ে নিচের Caused by খুঁজুন — মূল কারণ সাধারণত সেখানে।
  • ... 1 more: বাকি frame-গুলো ওপরের trace-এর সাথে হুবহু মিলে যায় (এখানে main), তাই আবার না ছেপে সংক্ষেপ করা হয়েছে।

আসল প্রজেক্টে trace-এ থাকবে কয়েক ডজন লাইন — Spring, Tomcat বা JDK-র ভেতরের method. কৌশল একটাই: নিজের package-এর প্রথম লাইনটি খুঁজুন। লাইব্রেরির ভেতরে bug থাকার সম্ভাবনা কম; সাধারণত ভুল argument গেছে আপনার কোড থেকেই।

helpful NullPointerException

Java 14-এ JEP 358 এনেছে বিস্তারিত NullPointerException বার্তা, Java 15 থেকে যা ডিফল্টে চালু। এখন বার্তায় লেখা থাকে কোন জিনিসটি null ছিল: Cannot invoke "String.length()" because "name" is null. তবে local variable-এর নাম আসে শুধু তখনই, যখন class-টি javac -g দিয়ে debug তথ্যসহ compile করা হয় (Maven/Gradle আর IDE সাধারণত তা করে); না হলে নামের জায়গায় দেখবেন "<local1>"-এর মতো লেখা — অনলাইন runner-এ সাধারণত এটাই আসে।

৪ · try, catch, finally — ধরা, সামলানো, পরিষ্কার করা

exception ধরতে হলে যে কোডে সেটি আসতে পারে তা try block-এ রাখুন, আর ঠিক পরে এক বা একাধিক catch লিখুন — একে বলে try-catch. প্রতিটি catch একটি টাইপ ধরে: সেই class বা তার যেকোনো subclass. শেষে ঐচ্ছিক finally block, যা সবসময় চলে।

try {
    int amount = Integer.parseInt(input);       // NumberFormatException আসতে পারে
    wallet.withdraw(amount);                    // InsufficientBalanceException আসতে পারে
} catch (NumberFormatException e) {
    System.out.println("সংখ্যা লিখুন");
} catch (InsufficientBalanceException e) {
    System.out.println("ব্যালেন্স কম: " + e.getMessage());
} finally {
    System.out.println("লেনদেনের চেষ্টা শেষ");     // সবসময় চলে
}

catch-এর ক্রম — ছোট (subclass) আগে, বড় পরে

exception এলে Java catch-গুলো ওপর থেকে নিচে পরীক্ষা করে, আর প্রথম যেটি মেলে সেটিই চলে — বাকিগুলো দেখে না। তাই বড় টাইপ (যেমন Exception) আগে রাখলে পরের ছোট টাইপের catch কখনো পৌঁছানোই যাবে না। Java এমন মরা কোড মেনে নেয় না:

try {
    int n = Integer.parseInt(args[0]);
    System.out.println(n);
} catch (Exception e) {
    System.out.println("something went wrong");
} catch (NumberFormatException e) {              // ✗ এখানে আর কখনো আসবে না
    System.out.println("not a number");
}
Main.java:8: error: exception NumberFormatException has already been caught
        } catch (NumberFormatException e) {
          ^
1 error

সমাধান: ক্রম উল্টে দিন — NumberFormatException আগে, Exception পরে। নিয়মটা গাছ থেকেই আসে: NumberFormatException হলো IllegalArgumentException-এর subclass, সেটি আবার RuntimeException-এর, সেটি Exception-এর।

multi-catch — একই সামলানো, কয়েকটি টাইপ

দুটি সম্পর্কহীন exception-এর জন্য একই কাজ করতে চাইলে Java 7 থেকে multi-catch আছে: catch (IllegalArgumentException | InsufficientBalanceException e). শর্ত একটাই — টাইপগুলো একে অপরের subclass হতে পারবে না, কারণ তাহলে ছোটটি লেখা অর্থহীন:

Main.java:5: error: Alternatives in a multi-catch statement cannot be related by subclassing
        } catch (NumberFormatException | IllegalArgumentException e) {
                                         ^
  Alternative NumberFormatException is a subclass of alternative IllegalArgumentException
1 error

finally — সবসময় চলে, এমনকি return-এর পরেও

finally-র প্রতিশ্রুতি হলো: try থেকে স্বাভাবিকভাবে বেরোন, exception নিয়ে বেরোন, অথবা return দিয়ে বেরোন — finally চলবেই (ব্যতিক্রম শুধু System.exit() বা JVM নিজেই বন্ধ হয়ে গেলে)। নিচের method-টি দুইবার ডাকা হয়: একবার ঠিক ইনপুটে, একবার ভুল ইনপুটে। শেষ ধাপে একটি ফাঁদও আছে:

1static int pay(String input) {2    try {3        int amt = Integer.parseInt(input);4        System.out.println("try: " + amt);5        return amt;6    } catch (NumberFormatException e) {7        System.out.println("catch: সংখ্যা নয়");8        return -1;9    } finally {10        System.out.println("finally");11    }12}
pay(…)-এর ভেতরে
input
exception
ফেরত দেওয়ার অপেক্ষায়
  1. pay("50") — try শুরু। parseInt সফল, amt = 50.
  2. পরের লাইন চলে। exception নেই, তাই try-এর কোড স্বাভাবিকভাবে এগোয়: try: 50 ছাপা হলো।
  3. return amt — কিন্তু এখনই ফেরত নয়। JVM ফেরত-মান ৫০ মনে রাখে, তারপর method ছাড়ার আগে finally চালাতে যায়।
  4. finally চলল, তারপর ৫০ ফেরত গেল। catch পুরোপুরি বাদ, কারণ কোনো exception আসেনি। আউটপুটের ক্রম: try, finally — তারপর ডাকার জায়গায় ৫০.
  5. pay("5o") — try-এর প্রথম লাইনেই exception. “5o”-তে শূন্যের বদলে ইংরেজি o. লাইন ৪ আর ৫ আর চলে না; সরাসরি catch খোঁজা শুরু।
  6. catch মিলে গেল। NumberFormatException ধরা হলো, বার্তা ছাপা হলো। exception এখন “সামলানো” — আর ওপরে যাবে না।
  7. catch থেকে return -1. আবারও — মান মনে রাখা হলো, কিন্তু finally আগে।
  8. finally চলল, তারপর -1 ফেরত। দুই পথেই finally ঠিক একবার চলেছে। পরিষ্কারের কাজ (লক ছাড়া, সংযোগ বন্ধ) এজন্যই এখানে রাখা হয়।
  9. ফাঁদ: finally-র ভেতরে return. finally-তে return 0; লিখলে সেটি try বা catch-এর মনে রাখা মান মুছে দেয় — আর try থেকে যদি এমন কোনো exception বেরোচ্ছিল যা কেউ ধরেনি, সেটিও নিঃশব্দে হারিয়ে যায়। কখনো finally থেকে return বা throw করবেন না।

শেষ ধাপের ফাঁদটি যাচাই করা: try { throw new IllegalStateException("lost!"); } finally { return 2; } লেখা method কোনো exception ছাড়াই 2 ফেরত দেয় — "lost!" চিরতরে উধাও। javac এটা ডিফল্টে আটকায় না; শুধু -Xlint:finally চালু করলে finally clause cannot complete normally সতর্কবার্তা দেয়। এটি swallowed exception-এর সবচেয়ে লুকোনো রূপ।

৫ · throw আর throws — ছোড়া আর আগাম ঘোষণা

দুটি শব্দ দেখতে প্রায় এক, কাজ সম্পূর্ণ আলাদা:

throwthrows
কীএকটি statement — এখনই exception ছোড়াmethod-এর ঘোষণার অংশ — এই exception বেরোতে পারে
কোথায় লেখেmethod-এর শরীরের ভেতরেparameter তালিকার পরে, {-এর আগে
পরে কী আসেএকটি object: throw new X(...)এক বা একাধিক টাইপ: throws A, B
উদাহরণthrow new IllegalArgumentException("amount")void withdraw(long a) throws InsufficientBalanceException

throw দিয়ে আপনি নিজে ব্যর্থতা ঘোষণা করেন। throws দিয়ে method তার চুক্তিতে লিখে রাখে কোন checked exception সে বাইরে পাঠাতে পারে — যেমন পর্ব ১২-এ শিখেছেন method-এর signature বলে সে কী নেয় আর কী ফেরত দেয়, তেমনি throws বলে সে কীভাবে ব্যর্থ হতে পারে।

compiler-এর পাহারা

checked exception ছোড়ে এমন method ডেকে যদি আপনি ধরেনও না, ঘোষণাও না করেন, compiler থামিয়ে দেয়। নিচের ফাইলের ১৪ নম্বর লাইনে main সরাসরি w.withdraw(800) ডেকেছে, কোনো try ছাড়া:

Main.java:14: error: unreported exception InsufficientBalanceException; must be caught or declared to be thrown
        w.withdraw(800);
                  ^
1 error

বার্তাটি নিজেই দুটি পথ বলে দিচ্ছে: caught — try/catch দিয়ে এখানেই সামলান; অথবা declared to be thrown — নিজের ঘোষণায় throws InsufficientBalanceException লিখে দায়িত্ব ওপরে পাঠান। কোনটা ঠিক, তা নির্ভর করে এই method সমস্যাটি সামলানোর মতো তথ্য রাখে কি না। ওয়ালেটের নিচের স্তরের কোড জানে না ব্যবহারকারীকে কী দেখাতে হবে, তাই ঘোষণা করে; UI-এর কাছাকাছি কোড ধরে।

উল্টো দিকেও compiler সতর্ক। যে checked exception try-এর ভেতরে কখনো আসতেই পারে না, তার জন্য catch লিখলে সেটাও error: exception InsufficientBalanceException is never thrown in body of corresponding try statement. unchecked-এর ক্ষেত্রে দুই দিকেই compiler চুপ — আপনি চাইলে throws IllegalArgumentException লিখতে পারেন নথি হিসেবে, কিন্তু বাধ্যতামূলক নয়।

compiler-কে চুপ করাতে throws Exception লিখে দেওয়া বা ফাঁকা catch বসানো সবচেয়ে সহজ — আর সবচেয়ে ক্ষতিকর। প্রথমটি চুক্তি থেকে সব তথ্য মুছে দেয় (ডাকার কোড আর জানে না কী আসতে পারে), দ্বিতীয়টি ভুলটাই লুকিয়ে ফেলে।

৬ · unchecked exception আর নিজের exception

প্রতিদিন যে unchecked exception-গুলো দেখবেন, সেগুলো চিনে রাখুন। প্রতিটির বার্তা Java 21-এ চালিয়ে যাচাই করা:

exceptionকখন আসেবার্তা (উদাহরণ)
ArithmeticExceptionint-কে শূন্য দিয়ে ভাগ (double-এ নয় — সেখানে Infinity)/ by zero
NumberFormatExceptionInteger.parseInt("12a"), ফাঁকা লেখাFor input string: "12a"
IndexOutOfBoundsException৩টি উপাদানের ArrayList-এ get(5)Index 5 out of bounds for length 3
ArrayIndexOutOfBoundsExceptionnew int[3]-এর a[5]Index 5 out of bounds for length 3
NullPointerExceptionnull reference-এ method বা fieldCannot invoke "String.length()" ...
IllegalArgumentExceptionআপনার method-এ অবৈধ argument (ঋণাত্মক টাকা)আপনার দেওয়া বার্তা
IllegalStateExceptionargument ঠিক, কিন্তু object-এর অবস্থা ভুল (বন্ধ ওয়ালেট)আপনার দেওয়া বার্তা
ClassCastExceptionভুল টাইপে cast (পর্ব ২০)class java.lang.String cannot be cast to class java.lang.Integer ...

এগুলোর বেশিরভাগ JVM বা JDK নিজে ছোড়ে। ব্যতিক্রম দুটি — IllegalArgumentException আর IllegalStateException — যা আপনি নিজের method-এ সবচেয়ে বেশি ব্যবহার করবেন, fail-fast নীতিতে: ভুল মান ঢোকামাত্র থামুন, গভীরে ঢুকে অদ্ভুত ফল দেওয়ার আগে। null যাচাইয়ের জন্য JDK-তে তৈরি সহজ হাতিয়ার আছে, Objects.requireNonNull:

Wallet(String owner, long balance) {
    this.owner = Objects.requireNonNull(owner, "owner must not be null");
    if (balance < 0) throw new IllegalArgumentException("balance < 0: " + balance);
    this.balance = balance;
}

null এলে সাথে সাথে java.lang.NullPointerException: owner must not be null — কোন জিনিস null ছিল, তা constructor-এর প্রথম লাইনেই স্পষ্ট। না হলে null চুপচাপ field-এ বসে থাকত আর অনেক পরে, সম্পূর্ণ অন্য জায়গায় NPE দিত।

নিজের exception — Exception না RuntimeException থেকে?

JDK-র exception দিয়ে কাজ না চললে নিজে বানান — একটি custom exception সাধারণত একটি ছোট class, যার constructor বার্তাটি super(...)-এ পাঠায়। আসল সিদ্ধান্ত হলো parent কে হবে:

  • extends Exception (checked): ঠিকঠাক কোডেও এটি ঘটতে পারে, আর ডাকার কোড বাস্তবে কিছু করতে পারে — ব্যবহারকারীকে বলা, কম টাকা পাঠানোর প্রস্তাব দেওয়া। InsufficientBalanceException এর আদর্শ উদাহরণ: ব্যালেন্স কম হওয়া bug নয়, ব্যবসার স্বাভাবিক একটি ফল।
  • extends RuntimeException (unchecked): এটি ঘটা মানে কোথাও প্রোগ্রামের ভুল, অথবা ডাকার কোড কিছুই করতে পারবে না শুধু থেমে যাওয়া ছাড়া। আধুনিক অনেক framework (যেমন Spring) প্রায় সব exception-ই unchecked রাখে।

এবার সব একসাথে — একটি ওয়ালেট, ব্যবহারকারীর লেখা চারটি ইনপুট, আর তিন ধরনের ব্যর্থতা। চালিয়ে দেখুন:

class InsufficientBalanceException extends Exception {
    InsufficientBalanceException(String message) { super(message); }
}
class Wallet {
    private long balance = 500;
    void withdraw(long amount) throws InsufficientBalanceException {
        if (amount <= 0) throw new IllegalArgumentException("পরিমাণ ধনাত্মক নয়: " + amount);
        if (amount > balance) throw new InsufficientBalanceException("ব্যালেন্স " + balance + ", চাওয়া " + amount);
        balance -= amount;
        System.out.println(amount + " টাকা তোলা হলো, বাকি " + balance);
    }
}
class Main {
    public static void main(String[] args) {
        var w = new Wallet();
        for (String input : new String[] {"200", "12a", "800", "-5"})
            try {
                w.withdraw(Long.parseLong(input));
            } catch (NumberFormatException e) {
                System.out.println("সংখ্যা নয়: " + e.getMessage());
            } catch (IllegalArgumentException | InsufficientBalanceException e) {
                System.out.println("বাতিল: " + e.getMessage());
            }
    }
}

আউটপুট চার লাইন: 200 টাকা তোলা হলো, বাকি 300, সংখ্যা নয়: For input string: "12a", বাতিল: ব্যালেন্স 300, চাওয়া 800, বাতিল: পরিমাণ ধনাত্মক নয়: -5. তিনটি জিনিস খেয়াল করুন। (১) NumberFormatException-এর catch আগে — কারণ সে IllegalArgumentException-এর subclass; ক্রম উল্টালে “has already been caught” error. (২) multi-catch-এ IllegalArgumentException আর InsufficientBalanceException সম্পর্কহীন, তাই একসাথে চলে। (৩) একটি ইনপুট ব্যর্থ হলেও লুপ থামেনি — exception প্রতিটি পুনরাবৃত্তির ভেতরেই ধরা হয়েছে, আর ৮০০-র চেষ্টায় ব্যালেন্স অক্ষত থেকেছে। (অনলাইন runner-এর জন্য কোনো class public নয়; নিজের প্রজেক্টে public class Main থাকে Main.java-তে।)

নতুন exception বানানোর আগে কোন পথে যাবেন, এই প্রশ্নগুলো ধাপে ধাপে জিজ্ঞেস করুন:

  1. এটা কি সত্যিই ব্যতিক্রম?না হলেসাধারণ ফেরত-মান
  2. ডাকার কোডের ভুল?হ্যাঁ হলেunchecked
  3. ডাকার কোড কিছু করতে পারবে?হ্যাঁ হলেchecked
  4. কেউই কিছু করতে পারবে না?তাহলেunchecked, ওপরে লগ
  5. JDK-তে আছে?আগে খুঁজুনতারপর নিজেরটা
প্রশ্ন ১“মান নেই” কি ব্যর্থতা?
findUser("01711...") → ব্যবহারকারী নেই
এটা স্বাভাবিক ফল, ব্যতিক্রম নয়
→ Optional<User> ফেরত দিন (পর্ব ৩৬)
প্রশ্ন ২argument বা অবস্থার ভুল
withdraw(-5), null owner, বন্ধ ওয়ালেট
ডাকার কোড ঠিক করলেই আর ঘটবে না
→ IllegalArgumentException / IllegalStateException
প্রশ্ন ৩সঠিক কোডেও ঘটে, সামলানো যায়
withdraw(800), ব্যালেন্স 500
ব্যবহারকারীকে জানানো, কম টাকার প্রস্তাব
→ InsufficientBalanceException extends Exception
প্রশ্ন ৪অ্যাপ চালু হওয়ার সময়ের config নেই
config ফাইল ভাঙা, ডেটাবেসের পাসওয়ার্ড ভুল
প্রতিটি স্তরে throws লিখে কোনো লাভ নেই
→ unchecked (cause সহ), সবচেয়ে ওপরে একবার লগ
প্রশ্ন ৫নতুন class বানানোর আগে
IllegalArgumentException, IllegalStateException,
UnsupportedOperationException, IOException...
চেনা নাম = সবাই বোঝে; নিজেরটা শুধু যখন
ডাকার কোডকে আলাদা করে ধরতে হবে
  1. আগে জিজ্ঞেস করুন — এটা কি আদৌ exception? খুঁজে না পাওয়া, তালিকা ফাঁকা থাকা প্রায়ই স্বাভাবিক ফল। তার জন্য exception ছুড়লে ডাকার কোডকে প্রতিবার try লিখতে হয়। “মান থাকতেও পারে, না-ও পারে” বোঝাতে Java 8 থেকে Optional আছে, পর্ব ৩৬-এ বিস্তারিত।
  2. ডাকার কোডের ভুল হলে unchecked. ঋণাত্মক টাকা, null নাম — ডাকার কোড ঠিক করলেই আর ঘটবে না। প্রতিটি caller-কে try লেখাতে বাধ্য করা অর্থহীন; JDK-র চেনা unchecked exception ছুড়ুন।
  3. সঠিক কোডেও ঘটে আর সামলানো যায় — checked. ব্যালেন্স কম, ফাইল নেই, সার্ভার সাড়া দিচ্ছে না। checked রাখলে compiler নিশ্চিত করে কেউ ভুলে যাবে না।
  4. ঘটতে পারে কিন্তু কেউই কিছু করতে পারবে না — unchecked. অ্যাপ শুরুর সময়ই config ভাঙা থাকলে মাঝের দশটি method-এ throws লেখা শুধু শব্দ বাড়ায়। unchecked exception-এ আসল কারণ (cause) মুড়ে দিন, আর সবচেয়ে ওপরের স্তরে একবার লগ করুন।
  5. নাম বাছুন — আগে JDK. চেনা exception সবাই এক নজরে বোঝে। নিজের class শুধু তখনই, যখন ডাকার কোডকে এই ব্যর্থতা আলাদা করে ধরতে হবে বা অতিরিক্ত তথ্য (ব্যালেন্স, চাওয়া পরিমাণ) বহন করতে হবে।

৭ · chaining আর ভালো অভ্যাস — exception নষ্ট করবেন না

অংশ ৩-এর stack trace-এ Caused by এসেছিল এক লাইনের কারণে: throw new IllegalStateException("transfer failed", e);. দ্বিতীয় argument e হলো cause — নতুন exception-এর ভেতরে পুরোনোটি রেখে দেওয়া। একে বলে exception chaining. এর উদ্দেশ্য দুটি: ওপরের স্তরকে তার নিজের ভাষায় একটি exception দেওয়া (“transfer ব্যর্থ”, ভেতরের ডেটাবেস-বিবরণ নয়), আর একই সাথে আসল কারণ না হারানো। e.getCause() দিয়ে ভেতরেরটি পাওয়া যায়। নিজের exception-এও তাই (String message, Throwable cause) constructor রাখুন, যা super(message, cause) ডাকে।

যে অভ্যাসগুলো exception-কে অকেজো করে

try {
    wallet.withdraw(amount);
} catch (InsufficientBalanceException e) {
    // পরে দেখব
}

ফাঁকা catch: ব্যর্থতা ঘটল, কিন্তু কেউ জানল না। ব্যবহারকারী ভাবলেন টাকা গেছে, লগে কিছু নেই। এটি debugging-এর সবচেয়ে কঠিন bug-গুলোর একটি — উপসর্গ দেখা দেয় অনেক পরে, অন্য জায়গায়। যদি সত্যিই উপেক্ষা করা নিরাপদ হয়, অন্তত একটি মন্তব্যে কারণ লিখুন আর ভ্যারিয়েবলের নাম দিন ignored.

try {
    processOrder(order);
} catch (Exception e) {
    e.printStackTrace();
}

catch (Exception e) শুধু আপনার প্রত্যাশিত ব্যর্থতা নয়, প্রতিটি bug-ও ধরে — NPE, ভুল index, সব। printStackTrace() stderr-এ ছাপে আর প্রোগ্রাম এমনভাবে চলতে থাকে যেন কিছুই হয়নি। catch (Throwable t) আরও খারাপ — OutOfMemoryError-ও ধরে। যা সামলাতে জানেন, শুধু সেটাই ধরুন।

try {
    wallet.withdraw(amount);
} catch (InsufficientBalanceException e) {
    ui.showError("ব্যালেন্স কম: " + e.getMessage());   // এখানে সামলানো যায়
}

try {
    repository.save(order);
} catch (IOException e) {
    throw new OrderFailedException("order " + id, e);  // নিজের ভাষায়, cause সহ
}

নিয়ম: প্রতিটি catch হয় সত্যিকারের সামলানো করবে (বিকল্প পথ, ব্যবহারকারীকে জানানো, আবার চেষ্টা), নয়তো cause সহ আবার ছুড়বে। একই exception একাধিক স্তরে লগ করবেন না — “log and rethrow” একই ভুল লগে বারবার দেখায়।

exception শুধু ব্যতিক্রমের জন্য — control flow নয়

কেউ কেউ লুপ থামাতে exception ব্যবহার করেন — array-এর শেষ পর্যন্ত চলতে থাকেন, আর ArrayIndexOutOfBoundsException ধরে লুপ থেকে বেরোন। এটা কাজ করে, কিন্তু তিন কারণে ভুল। প্রথমত, পড়তে বিভ্রান্তিকর — catch দেখলে পাঠক ভাবেন কিছু ভুল হয়েছে। দ্বিতীয়ত, সত্যিকারের bug লুকিয়ে যায়: লুপের ভেতরে অন্য কোনো index ভুল হলে সেটাও “লুপ শেষ” বলে ধরা পড়বে। তৃতীয়ত, ধীর।

ধীর কেন? exception object তৈরির সময় Throwable-এর constructor fillInStackTrace() ডাকে, যা সেই মুহূর্তের পুরো call stack হেঁটে রেকর্ড করে। stack যত গভীর, খরচ তত বেশি — আর তারপর unwinding-এর খরচ। একবারের ব্যর্থতায় এটা নগণ্য; প্রতি সেকেন্ডে লক্ষবার হলে চোখে পড়ার মতো। তাই নিয়ম: যা আগে থেকে যাচাই করা যায় (index সীমার মধ্যে কি না, ইনপুট খালি কি না), তা if দিয়ে যাচাই করুন; exception রাখুন সত্যিকারের অপ্রত্যাশিত ঘটনার জন্য।

যা ঘটার কথা — if দিয়ে।
যা ঘটার কথা নয় — exception দিয়ে।

৮ · try-with-resources — খোলা জিনিস নিশ্চিতভাবে বন্ধ

ফাইল, ডেটাবেস সংযোগ, নেটওয়ার্ক socket — এগুলো খোলার পর বন্ধ করা বাধ্যতামূলক। না করলে operating system-এর সীমিত file handle বা ডেটাবেসের সংযোগ ফুরিয়ে যায়, আর সার্ভার কয়েক ঘণ্টা পর রহস্যজনকভাবে থেমে যায়। Java 7-এর আগে এটা লিখতে হতো finally দিয়ে, null-যাচাই আর close()-এর নিজের exception-এর জন্য আরেকটি try সহ — দশ লাইনের অনুষ্ঠান, যা প্রায়ই ভুল লেখা হতো।

Java 7 আনল try-with-resources: try-এর পরের বন্ধনীতে সম্পদ খুলুন, block শেষ হলে — স্বাভাবিকভাবে বা exception নিয়ে — Java নিজেই close() ডাকবে। শর্ত একটাই: object-টিকে AutoCloseable implement করতে হবে, যে interface-এ শুধু একটি method: void close() throws Exception. JDK-র প্রায় সব stream, reader, JDBC সংযোগ এটি করে। নিজের class-ও করতে পারে — নিচে একটি নকল “config” আর “log” সম্পদ, যারা খোলা আর বন্ধ হওয়ার সময় ছাপে। log বন্ধ করার সময় নিজেও একটি exception দেয়:

  1. খোলাconfig, logলেখার ক্রমে
  2. bodythrowমূল exception
  3. বন্ধlog, configউল্টো ক্রমে
  4. catchgetSuppressed()কিছুই হারায়নি
  5. finallyসবশেষেresource ইতিমধ্যে বন্ধ
ধাপ ১try (var config = …; var log = …)
খোলা হলো: config
খোলা হলো: log
ধাপ ২body চলছে
config পড়া হচ্ছে...
throw IllegalArgumentException("লাইন 3 ভুল")
→ এখনই catch নয়; আগে resource বন্ধ
ধাপ ৩শেষে খোলা হয়েছিল যে, সে আগে বন্ধ
বন্ধ হলো: log
log.close() নিজেও ছুড়ল:
IllegalStateException("log বন্ধ করতে সমস্যা")
ধাপ ৪দ্বিতীয় exception কোথায় যায়?
মূল: IllegalArgumentException("লাইন 3 ভুল")
  └─ suppressed: IllegalStateException("log ...")
তারপর: বন্ধ হলো: config
ধাপ ৫catch মূল exception পায়
ধরা হলো: লাইন 3 ভুল
  suppressed: log বন্ধ করতে সমস্যা
ধাপ ৬সবশেষে
finally চলল
ক্রম: খোলা → body → বন্ধ (উল্টো) → catch → finally
  1. লেখার ক্রমে খোলা। প্রথমে config, তারপর log. কোনো একটি খুলতে গিয়েই exception হলে, যেগুলো ইতিমধ্যে খোলা হয়েছে শুধু সেগুলোই বন্ধ হবে।
  2. body-তে exception. এটাই মূল exception. কিন্তু catch-এ লাফ দেওয়ার আগে Java resource-গুলো বন্ধ করে — catch চলার সময় সবকিছু ইতিমধ্যে পরিষ্কার।
  3. উল্টো ক্রমে বন্ধ — আগে log. শেষে যা খোলা হয়েছে তা প্রায়ই আগেরটির ওপর নির্ভর করে (যেমন ফাইলের ওপর বসানো reader), তাই সে আগে বন্ধ হয় — stack-এর মতো। কিন্তু log.close() নিজেও exception দিল।
  4. দ্বিতীয় exception হারায় না — suppressed হয়। পুরোনো finally-কৌশলে close-এর exception মূলটিকে চাপা দিত, আর আসল কারণ হারিয়ে যেত। try-with-resources মূলটিকেই রাখে আর close-এরটিকে তার সাথে suppressed exception হিসেবে জুড়ে দেয়। তারপর config-ও বন্ধ হয় — একটির ব্যর্থতায় অন্যটির বন্ধ হওয়া আটকায় না।
  5. catch পায় মূল exception. e.getSuppressed() একটি array দেয়, যাতে close-এর exception-গুলো আছে। না-ধরা অবস্থায় stack trace-এও Suppressed: শিরোনামে এগুলো ছাপা হয়।
  6. finally সবশেষে। try-with-resources-এর নিজের catch আর finally চলে resource বন্ধ হওয়ার পরে. তাই finally-তে resource ব্যবহার করতে যাবেন না — সেগুলো আর খোলা নেই।
record Resource(String name, boolean failOnClose) implements AutoCloseable {
    Resource {
        System.out.println("খোলা হলো: " + name);
    }
    @Override public void close() {
        System.out.println("বন্ধ হলো: " + name);
        if (failOnClose) throw new IllegalStateException(name + " বন্ধ করতে সমস্যা");
    }
}
class Main {
    public static void main(String[] args) {
        try (var config = new Resource("config", false);
             var log = new Resource("log", true)) {
            System.out.println("config পড়া হচ্ছে...");
            throw new IllegalArgumentException("লাইন 3 ভুল");
        } catch (IllegalArgumentException e) {
            System.out.println("ধরা হলো: " + e.getMessage());
            for (Throwable s : e.getSuppressed())
                System.out.println("  suppressed: " + s.getMessage());
        } finally {
            System.out.println("finally চলল");
        }
    }
}

আউটপুট আট লাইন, ঠিক stepper-এর ক্রমে: খোলা হলো: config, খোলা হলো: log, config পড়া হচ্ছে..., বন্ধ হলো: log, বন্ধ হলো: config, ধরা হলো: লাইন 3 ভুল, suppressed: log বন্ধ করতে সমস্যা, finally চলল. সংক্ষেপের জন্য Resource এখানে একটি record (পর্ব ২৫), যার compact constructor খোলার বার্তা ছাপে — record-ও interface implement করতে পারে। খেয়াল করুন: close()-এর ঘোষণায় throws Exception নেই — override করার সময় checked exception কমিয়ে দেওয়া বৈধ, আর তাতে এই try-কে কোনো checked exception ধরতে হয় না। var দিয়ে resource ঘোষণাও চলে। Java 9 থেকে আগে থেকে থাকা effectively final ভ্যারিয়েবলও সরাসরি বসানো যায়: try (config) { ... }.

বাস্তবে ফাইল পড়া ঠিক এই কাঠামোতেই হয় — try (var reader = Files.newBufferedReader(path)) { ... } — আর সেখানে IOException checked, তাই catch বা throws লাগবে। java.nio.file দিয়ে আসল ফাইল নিয়ে কাজ পর্ব ৩৭-এ।

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

C-তে exception নেই — ফেরত-মান আর errno দিয়ে চলে। C++-এ try/catch/throw আছে, কিন্তু checked exception নেই আর finally-ও নেই; সেখানে destructor দিয়ে RAII সম্পদ বন্ধ করে — Java-র try-with-resources সেই ধারণারই কাছাকাছি, শুধু scope-এর জায়গায় স্পষ্ট block. Python-এর try/except/finally প্রায় হুবহু Java-র মতো, raise মানে throw, আর with মানে try-with-resources; কিন্তু Python-এ কোনো exception ঘোষণা বা ধরা বাধ্যতামূলক নয়। তুলনার জন্য দেখুন Python-এর exception আর C++-এর exception পর্ব।

পরীক্ষা ও ইন্টারভিউতে
  • checked আর unchecked-এর পার্থক্য? — checked (RuntimeException বাদে Exception-এর বংশধর) ধরা বা throws-এ ঘোষণা বাধ্যতামূলক, compiler যাচাই করে; unchecked (RuntimeException ও Error) নয়।
  • throw আর throws? — throw একটি object এখনই ছোড়ে; throws method-এর ঘোষণায় জানায় কোন exception বেরোতে পারে।
  • try-তে return থাকলে finally চলে? — হ্যাঁ, return-এর আগে; আর finally-তে নিজের return থাকলে সেটাই জেতে (তাই কখনো লিখবেন না)।
  • Error কি ধরা উচিত? — সাধারণত না; OutOfMemoryError বা StackOverflowError-এর পর অ্যাপের অবস্থা অনির্ভরযোগ্য।
  • suppressed exception কী? — try-with-resources-এ মূল exception-এর পর close() থেকে আসা exception, যা মূলটির সাথে জুড়ে থাকে; getSuppressed() দিয়ে পাওয়া যায়।

৯ · AI-এর যুগে — exception সামলানোর কোড রিভিউ করুন

AI-কে কোনো ফাইল পড়া বা API ডাকার কোড লিখতে বললে প্রায়ই দেখবেন: প্রতিটি method-এর চারপাশে try { ... } catch (Exception e) { e.printStackTrace(); }. কোড compile হয়, demo-তে চলে, আর প্রথম আসল ব্যর্থতায় প্রোগ্রাম ভুল অবস্থায় চলতে থাকে — কোনো লগ ফাইলে কিছু নেই, শুধু কোথাও stderr-এ কয়েকটি লাইন যা কেউ দেখে না। এই অভ্যাস এসেছে ইন্টারনেটের অসংখ্য টিউটোরিয়াল আর উত্তর থেকে, যেখানে উদ্দেশ্য ছিল শুধু compiler-কে চুপ করানো।

AI যা করেকেন সমস্যাআপনি যা চাইবেন
catch (Exception e) { e.printStackTrace(); }সব bug ধরে, প্রোগ্রাম ভুল অবস্থায় চলে; লগিং ব্যবস্থার বাইরে ছাপেনির্দিষ্ট টাইপ ধরা, logger বা cause সহ আবার ছোড়া
ফাঁকা catch বা // ignoreব্যর্থতা নিঃশব্দে হারায়হয় সামলানো, নয় ছোড়া; উপেক্ষা হলে কারণ লেখা
সব checked exception-কে new RuntimeException(e)-এ মুড়ে দেয়চুক্তি থেকে তথ্য মুছে যায়; ডাকার কোড আর আলাদা করে ধরতে পারে নাঅর্থপূর্ণ নিজের exception, অথবা সত্যিই সামলানো গেলে checked রাখা
ব্যবসার স্বাভাবিক ফলের জন্য exception (ব্যবহারকারী নেই → UserNotFoundException দিয়ে লুপ)ধীর, পড়তে বিভ্রান্তিকরOptional বা সাধারণ ফেরত-মান
finally-তে হাতে close()close-এর exception মূলটিকে চাপা দেয়; null-যাচাই ভুলে যায়try-with-resources

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

আমি Java 21 শিখছি, exception আর try-with-resources পর্যন্ত জানি। কাজ: [যেমন — একটি config resource পড়ে ওয়ালেটের দৈনিক সীমা বের করা] শর্ত: ১. catch (Exception e) বা catch (Throwable t) লিখবে না; শুধু সেই টাইপ ধরবে যা এখানে সত্যিই সামলানো যায়। ২. কোনো ফাঁকা catch বা শুধু e.printStackTrace() থাকবে না — হয় সামলাও, নয় cause সহ আবার ছোড়ো। ৩. প্রতিটি নিজের exception-এর পাশে এক লাইনে বলো কেন checked বা unchecked. ৪. close করতে হয় এমন সবকিছু try-with-resources-এ। ৫. স্বাভাবিক "পাওয়া যায়নি" ফলের জন্য exception নয়, Optional. ৬. শেষে একটি টেবিল: কোন method কোন exception ছোড়ে, আর কে সেটা ধরে।

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

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

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

প্র ০১checked exception Java-র নিজস্ব পরীক্ষা। পরের C#, Kotlin, Scala কেন সেটি নিল না — পরীক্ষাটি কি ব্যর্থ?

শিকড়। exception-কে procedure-এর ঘোষণার অংশ করার চিন্তা পুরোনো। ১৯৭০-এর দশকে Barbara Liskov-এর CLU ভাষায় procedure কোন কোন “signal” দিতে পারে তা ঘোষণায় লেখা হতো; পরে Modula-3-তে procedure-এর ঘোষণায় RAISES তালিকা ছিল। Java-র নকশাকারীরা এই ধারণাকে নিয়ে গেলেন এক ধাপ সামনে: compiler-ই নিশ্চিত করবে যে প্রতিটি checked exception হয় ধরা হয়েছে, নয় ঘোষণা করা হয়েছে। লক্ষ্য ছিল মহৎ — C-এর মতো ফেরত-মান যাচাই করতে ভুলে যাওয়া আর সম্ভব হবে না; ব্যর্থতার পথগুলো টাইপ সিস্টেমের অংশ হবে।

Hejlsberg-এর সমালোচনা। ২০০৩ সালে Artima ওয়েবসাইটে Bill Venners আর Bruce Eckel-কে দেওয়া সাক্ষাৎকার “The Trouble with Checked Exceptions”-এ C#-এর প্রধান নকশাকার Anders Hejlsberg ব্যাখ্যা করেন কেন C# এগুলো নেয়নি। তাঁর দুটি মূল যুক্তি: versioning — একটি method-এর পরের সংস্করণে নতুন একটি exception যোগ করলে throws বদলায়, আর সব ডাকার কোড ভেঙে যায়; আর scalability — ছোট উদাহরণে দুটি exception সামলানো সহজ, কিন্তু বড় সিস্টেমে ওপরের স্তরে চার-পাঁচটি subsystem-এর কয়েক ডজন exception জমতে পারে, আর প্রোগ্রামাররা বিরক্ত হয়ে throws Exception লেখেন বা ফাঁকা catch বসান — অর্থাৎ নিয়মটি যে সমস্যা দূর করতে চেয়েছিল, সেটাই তৈরি করে। তাঁর পর্যবেক্ষণ ছিল, ভালো কোডে try/catch-এর তুলনায় try/finally (পরিষ্কার করা) অনেক বেশি থাকে — তাঁর আন্দাজে প্রায় দশ গুণ; বেশিরভাগ exception একটি কেন্দ্রীয় জায়গায় সামলানো হয়।

পরের ভাষাগুলো। Scala আর Kotlin দুটিই JVM-এ চলে, তবু checked exception রাখেনি; Kotlin-এ @Throws annotation আছে শুধু Java কোডের সাথে মেলানোর জন্য। Java নিজেও পরে অস্বস্তিতে পড়ে: Java 8-এর lambda আর Stream-এর Function-এর মতো functional interface checked exception ছুড়তে পারে না, তাই lambda-র ভেতরে ফাইল পড়তে গেলে মুড়িয়ে ফেলতে হয় — এজন্যই Java 8-এ UncheckedIOException যোগ হয়েছে।

তবু ব্যর্থ নয়। সমর্থকেরা বলেন, সমস্যা ধারণায় নয়, অপব্যবহারে: যেখানে ব্যর্থতা সত্যিই প্রত্যাশিত আর সামলানো যায়, সেখানে checked exception দারুণ কাজ করে। মজার ব্যাপার, Rust-এর Result বা Swift-এর throws চিহ্ন অনেকটা একই ধারণা — ব্যর্থতা signature-এ দৃশ্যমান — শুধু আরও হালকা রূপে।

মূল উপলব্ধি: checked exception দেখায় যে একটি ভালো নিয়মও খারাপ হয়ে যায় যদি তা মানার খরচ বেশি হয় — মানুষ তখন নিয়মটি ফাঁকি দেওয়ার পথ খোঁজে। তাই checked রাখুন শুধু সেখানে, যেখানে ডাকার কোড সত্যিই কিছু করতে পারে।

প্র ০২Tony Hoare null-কে বলেছেন তাঁর “billion-dollar mistake”. NullPointerException কেন Java-র সবচেয়ে পরিচিত exception, আর ভাষাটি কীভাবে এর সাথে লড়েছে?

ইতিহাস। ব্রিটিশ কম্পিউটার বিজ্ঞানী Tony Hoare (quicksort-এর আবিষ্কারক) ১৯৬৫ সালে ALGOL W ভাষার নকশায় null reference যোগ করেন। ২০০৯ সালে লন্ডনের QCon সম্মেলনে তিনি এটিকে বলেন তাঁর “billion-dollar mistake” — কারণ বাস্তবায়ন সহজ ছিল বলে তিনি এটি রেখেছিলেন, আর পরের দশকগুলোতে এর ফলে অগণিত bug, crash আর নিরাপত্তা-ছিদ্র তৈরি হয়েছে। সংখ্যাটি প্রতীকী, হিসাব করা নয়।

Java-য় কেন এত বেশি। Java-তে প্রতিটি reference টাইপের ভ্যারিয়েবল null হতে পারে, আর টাইপ সিস্টেম String আর “হয়তো null এমন String”-এর মধ্যে পার্থক্য করে না। Compiler তাই সাহায্য করতে পারে না; ভুলটা ধরা পড়ে শুধু চলার সময়, NullPointerException হয়ে। এটি unchecked — কারণ প্রায় প্রতিটি লাইনেই ঘটতে পারে, checked হলে সবকিছুই try-তে মুড়তে হতো।

লড়াইয়ের ধাপ। Java 7-এ এল Objects.requireNonNull — null-কে সীমানাতেই থামানো (fail-fast). Java 8 আনল Optional — ফেরত-মানের টাইপেই লেখা “না-ও থাকতে পারে”। Java 14-এর JEP 358 (Java 15 থেকে ডিফল্টে চালু) NPE-র বার্তায় বলে দেয় ঠিক কোন জিনিসটি null ছিল — আগে শুধু লাইন নম্বর আসত, আর a.b().c().d()-এর মতো লাইনে কোনটা null তা অনুমান করতে হতো। এর বাইরে @Nullable/@NonNull annotation আর বাইরের static analysis টুল আছে, কিন্তু তা ভাষার অংশ নয়।

অন্য ভাষার সমাধান। Kotlin টাইপ সিস্টেমেই ভাগ করেছে: String কখনো null নয়, String? হতে পারে, আর compiler যাচাই না করে String?-এর method ডাকতে দেয় না। Swift-এর optional আর Rust-এর Option একই চিন্তার। C#-ও পরে nullable reference type যোগ করেছে। Java-তেও null-সংক্রান্ত টাইপ চিহ্ন নিয়ে আলোচনা আর প্রস্তাব চলছে, তবে এই লেখার সময় তা স্থায়ী বৈশিষ্ট্য নয়।

মূল উপলব্ধি: NPE আসলে একটি নকশার সিদ্ধান্তের দাম — যে সিদ্ধান্ত “কোনো মান নেই” কথাটি টাইপের বাইরে রেখে দিয়েছিল। আপনার হাতে থাকা প্রতিকার: সীমানায় requireNonNull, ফেরত-মানে Optional, আর null ফেরত না দেওয়ার অভ্যাস।

প্র ০৩Go আর Rust exception ছাড়াই ব্যর্থতা সামলায় — error-কে সাধারণ ফেরত-মান বানিয়ে। কোন পথ ভালো?

Go-র পথ। ২০০৯ সালে Google-এ প্রকাশিত Go ভাষায় ব্যর্থ হতে পারে এমন function সাধারণত দুটি মান ফেরত দেয় — ফল আর একটি error. ডাকার কোড লেখে if err != nil { return err }. Go-র নকশাকারীদের যুক্তি: error আলাদা কিছু নয়, সাধারণ মান; প্রতিটি ব্যর্থতার পথ কোডে চোখের সামনে থাকে, কোনো লুকোনো লাফ নেই। দাম হলো পুনরাবৃত্তি — Go কোডের বড় অংশ এই তিন লাইনের যাচাই। আর ভুলে যাওয়ার সুযোগও আছে: err উপেক্ষা করা যায়। সত্যিকারের অসম্ভব অবস্থার জন্য Go-তেও panic/recover আছে, যা exception-এর মতোই stack unwind করে।

Rust-এর পথ। Rust ফেরত দেয় Result<T, E> — একটি result type, যা হয় Ok(মান) নয় Err(ভুল). ভেতরের মান পেতে হলে দুই ক্ষেত্রই সামলাতে হয়, compiler তা নিশ্চিত করে — অনেকটা Java-র checked exception-এর মতো কঠোর। কিন্তু ? অপারেটর (Rust 1.13, ২০১৬) একটি অক্ষরে “ভুল হলে ওপরে পাঠাও” লেখার সুযোগ দেয়, তাই Go-র মতো পুনরাবৃত্তি নেই। প্রোগ্রামিংয়ের ভুলের জন্য Rust-এ আছে panic! — যা সাধারণত ধরা হয় না।

লক্ষ্য করার মতো মিল। তিনটি ভাষাই আসলে একই ভাগ করছে: প্রত্যাশিত, সামলানো যায় এমন ব্যর্থতা (Java-র checked, Go-র error, Rust-এর Result) আর প্রোগ্রামের bug (Java-র RuntimeException, Go আর Rust-এর panic). Microsoft-এর গবেষণামূলক Midori প্রজেক্টে কাজ করা Joe Duffy ২০১৬ সালে “The Error Model” প্রবন্ধে ঠিক এই ভাগটিকেই কেন্দ্রীয় করেছেন: bug হলে দ্রুত থামো, প্রত্যাশিত ব্যর্থতা টাইপে দেখাও।

exception-এর পক্ষে। exception মাঝের স্তরগুলোকে পরিষ্কার রাখে আর stack trace বিনা খরচে দেয় — Go বা Rust-এ error-এর উৎস খুঁজতে আলাদা ব্যবস্থা লাগে। বিপক্ষে: কোন লাইন থেকে লাফ আসতে পারে তা কোড দেখে বোঝা যায় না।

মূল উপলব্ধি: প্রশ্নটা “exception না ফেরত-মান” নয়; আসল প্রশ্ন হলো প্রত্যাশিত ব্যর্থতা আর bug-কে আলাদা করা। Java-তে সেই ভাগের হাতিয়ার checked বনাম unchecked, আর “মান নেই”-এর জন্য Optional.

প্র ০৪Joshua Bloch-এর Effective Java-র exception অধ্যায় পেশাদার Java-র নিয়মবই বলে পরিচিত। মূল পরামর্শগুলো কী, আর কেন?

লেখক। Joshua Bloch Sun Microsystems-এ Java Collections Framework-এর নকশা করেছিলেন, পরে Google-এ কাজ করেন। তাঁর Effective Java (প্রথম সংস্করণ ২০০১, তৃতীয় ২০১৮) বই প্রতিটি পরামর্শকে একটি “Item” হিসেবে লেখে। তৃতীয় সংস্করণে exception নিয়ে একটি পুরো অধ্যায় আছে।

শুধু ব্যতিক্রমী অবস্থায় exception. প্রথম পরামর্শই হলো exception দিয়ে সাধারণ control flow না চালানো। Bloch-এর বিখ্যাত উদাহরণ: array-এর শেষ পর্যন্ত লুপ চালিয়ে ArrayIndexOutOfBoundsException ধরে থামা — কেউ ভাবতেন এটা দ্রুত, কারণ সীমা-যাচাই বাদ যায়; আসলে JVM সাধারণ লুপকে অনেক ভালোভাবে optimize করে, আর exception-এর পথ ধীর, তার ওপর লুকোনো bug-এর ঝুঁকি।

checked বনাম runtime. সবচেয়ে উদ্ধৃত পরামর্শ: যে অবস্থা থেকে ডাকার কোড ফিরতে পারে, তার জন্য checked; প্রোগ্রামিংয়ের ভুলের জন্য runtime exception. সাথে সতর্কতা: অপ্রয়োজনীয় checked exception এড়ান — ডাকার কোড যদি throw new AssertionError() ছাড়া কিছুই করতে না পারে, তাহলে checked রেখে তাকে কষ্ট দেওয়ার মানে নেই।

বাকি পরামর্শ। নিজের বানানোর আগে JDK-র চেনা exception (IllegalArgumentException, IllegalStateException, UnsupportedOperationException) ব্যবহার করুন। নিচের স্তরের exception ওপরের স্তরের ভাষায় অনুবাদ করুন (exception translation, cause সহ — এই পর্বের chaining). প্রতিটি method কী ছোড়ে, Javadoc-এ লিখুন। বার্তায় ব্যর্থতার তথ্য রাখুন — শুধু “invalid” নয়, কোন মান আর সীমা কত। ব্যর্থ হলে object-কে আগের অবস্থায় রাখুন (failure atomicity) — যেমন এই পর্বের ওয়ালেট আগে যাচাই করে তারপর ব্যালেন্স বদলায়। আর শেষটি: exception উপেক্ষা করবেন না — ফাঁকা catch লিখলে অন্তত মন্তব্যে কারণ লিখুন।

মূল উপলব্ধি: Bloch-এর পরামর্শগুলোর সারকথা একটাই — exception হলো API-র চুক্তির অংশ। কী ছোড়া হবে, কোন টাইপে, কোন বার্তায়, আর ব্যর্থতার পর object কোন অবস্থায় থাকবে — সবকিছু নকশার সিদ্ধান্ত, দুর্ঘটনা নয়।

প্র ০৫exception তৈরি করা কেন ব্যয়বহুল, আর HotSpot JVM কখনো কখনো stack trace ছাড়াই exception দেয় কেন?

খরচ কোথায়। একটি exception object বানানো আসলে সাধারণ object বানানোর মতোই — কিন্তু Throwable-এর constructor ভেতরে fillInStackTrace() ডাকে। এই method সেই মুহূর্তের পুরো call stack হেঁটে প্রতিটি frame-এর তথ্য রেকর্ড করে। Spring-এর মতো framework-এ stack প্রায়ই একশো frame-এর বেশি গভীর, তাই খরচ উল্লেখযোগ্য। তারপর আছে throw-এর খরচ: JVM-কে handler খুঁজতে frame ধরে ধরে নামতে হয়, আর JIT-এর কিছু optimization এই পথে কাজ করে না। stack trace ছাপার সময় (getStackTrace()) class আর লাইনের নাম বের করার আরেকটি খরচ আছে।

stack trace বাদ দেওয়ার উপায়। Java 7 থেকে Throwable-এ একটি protected constructor আছে: (String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace). শেষটি false দিলে stack trace রেকর্ড হয় না। পুরোনো কৌশল ছিল নিজের exception class-এ fillInStackTrace() override করে শুধু this ফেরত দেওয়া। খুব উচ্চ-গতির কিছু লাইব্রেরি এভাবে আগে থেকে তৈরি একটি exception বারবার ছোড়ে। দাম স্পষ্ট: ব্যর্থতা হলে কোথায় হলো, তা আর জানা যায় না।

OmitStackTraceInFastThrow. HotSpot JVM-এর একটি optimization আছে, যা ডিফল্টে চালু: কিছু “implicit” exception — যেমন NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException, ClassCastException — যদি JIT-compiled কোডের একই জায়গা থেকে বারবার ঘটে, JVM এক সময় নতুন exception না বানিয়ে আগে থেকে রাখা একটি exception ছুড়তে পারে, যার কোনো stack trace বা বার্তা থাকে না। Production লগে তখন হঠাৎ দেখা যায় শুধু java.lang.NullPointerException, নিচে একটিও at লাইন নেই — নতুনদের জন্য রহস্য। সমাধান: লগের আগের দিকে খুঁজুন, প্রথম কয়েকবার পুরো trace ছাপা হয়েছিল; অথবা JVM চালান -XX:-OmitStackTraceInFastThrow দিয়ে, যা এই optimization বন্ধ করে।

ব্যবহারিক অর্থ। এসবের মানে এই নয় যে exception এড়িয়ে চলতে হবে। সত্যিকারের ব্যর্থতায় stack trace-এর মূল্য তার খরচের চেয়ে অনেক বেশি। সমস্যা হয় শুধু যখন exception প্রতিদিনের স্বাভাবিক পথে লক্ষবার ঘটে — অর্থাৎ যখন সেটি control flow হিসেবে ব্যবহার হচ্ছে।

মূল উপলব্ধি: stack trace হলো ব্যর্থতার মুহূর্তের একটি ছবি, আর ছবি তোলার খরচ আছে। দুর্লভ ঘটনায় সেই খরচ দিন নির্দ্বিধায়; প্রতিদিনের পথে exception দেখলে নকশা আবার ভাবুন।

অনুশীলন

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

  1. আউটপুট অনুমান করুন: অংশ ৬-এর ওয়ালেট প্রোগ্রামে ইনপুটের array বদলে {"", "500", "1"} করুন। না চালিয়ে তিনটি লাইন লিখুন, তারপর চালিয়ে মিলিয়ে নিন।

    সংখ্যা নয়: For input string: "" — ফাঁকা লেখা সংখ্যা নয়, তাই Long.parseLong NumberFormatException দেয়। তারপর 500 টাকা তোলা হলো, বাকি 0 — ৫০০ ঠিক ব্যালেন্সের সমান, শর্ত amount > balance মিথ্যা। শেষে বাতিল: ব্যালেন্স 0, চাওয়া 1. শিক্ষা: সীমার মান (০, ঠিক সমান, ফাঁকা) পরীক্ষা করাই bug ধরার সবচেয়ে দ্রুত উপায়।

  2. checked থেকে unchecked: একই প্রোগ্রামে InsufficientBalanceException extends Exception-কে extends RuntimeException করুন। কী কী আর লাগবে না? কী হারালেন?

    প্রোগ্রাম আগের মতোই compile হয় আর একই আউটপুট দেয় — multi-catch-এ দুটি টাইপ এখনো সম্পর্কহীন (দুটিই RuntimeException-এর সন্তান, কিন্তু একটি অন্যটির subclass নয়)। withdraw-এর throws এখন ঐচ্ছিক; মুছে দিলেও কিছু ভাঙবে না। কী হারালেন: এখন কেউ withdraw ডেকে catch লিখতে ভুলে গেলে compiler কিছু বলবে না, আর প্রথম কম-ব্যালেন্সের ঘটনায় exception সোজা ওপরে উঠে যাবে। ব্যালেন্স কম হওয়া ব্যবসার স্বাভাবিক ফল, তাই এখানে checked রাখাই যুক্তিসংগত।

  3. দুটি suppressed: অংশ ৮-এর প্রোগ্রামে config-কেও new Resource("config", true) করুন। এখন কয়টি suppressed exception আসবে, আর কোন ক্রমে?

    দুটি, এই ক্রমে: suppressed: log বন্ধ করতে সমস্যা, তারপর suppressed: config বন্ধ করতে সমস্যা. resource বন্ধ হয় উল্টো ক্রমে (আগে log), আর প্রতিটি close-এর exception যে ক্রমে ঘটে সেই ক্রমেই মূল exception-এর সাথে জোড়া হয়। গুরুত্বপূর্ণ: log বন্ধ করতে ব্যর্থ হলেও config-এর close() ঠিকই ডাকা হয়েছে, আর মূল exception লাইন 3 ভুল-ই catch-এ পৌঁছেছে।

  4. রিভিউ করে আবার লিখুন: নিচের কোডে অন্তত তিনটি সমস্যা খুঁজুন, তারপর ঠিক করে লিখুন:
    static int parseAge(String s) {
        try {
            return Integer.parseInt(s);
        } catch (Exception e) {
        }
        return 0;
    }

    সমস্যা: (১) catch (Exception e) — শুধু NumberFormatException নয়, সব ধরে; (২) catch ফাঁকা — ব্যর্থতা গিলে ফেলা হয়েছে; (৩) 0 একটি বৈধ বয়সের মতো দেখায় — ডাকার কোড বুঝতেই পারবে না যে ইনপুট ভুল ছিল; (৪) ঋণাত্মক বা অবাস্তব বয়স যাচাই নেই। একটি ভালো রূপ:

    static int parseAge(String s) {
        int age;
        try {
            age = Integer.parseInt(s.strip());
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("বয়স সংখ্যা নয়: " + s, e);
        }
        if (age < 0 || age > 150) throw new IllegalArgumentException("অবাস্তব বয়স: " + age);
        return age;
    }

    এখন ভুল ইনপুট স্পষ্ট exception দেয়, আসল কারণ cause-এ থাকে। আর যদি “বয়স না-ও থাকতে পারে” স্বাভাবিক হয়, তাহলে exception-এর বদলে Optional<Integer> ফেরত দেওয়া যায় (পর্ব ৩৬)।

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

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

ক্রমJava-তে দুটি resource-ওয়ালা try-with-resources-এর body exception ছুড়লে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📂 resource-গুলো লেখার ক্রমে খোলা হয়
  2. 💥 body-তে exception ছোড়া হয়
  3. 🔒 resource-গুলো খোলার উল্টো ক্রমে বন্ধ হয়
  4. 🧯 মিলে যাওয়া catch block চলে
  5. 🏁 finally block চলে

Java-তে একটি try-এর পরে প্রথমে catch (Exception e), তারপর catch (NumberFormatException e) লিখলে কী হয়?

  • NumberFormatException এলে দুটি catch-ই চলে
  • প্রোগ্রাম চলে, শুধু দ্বিতীয় catch কখনো চলে না
  • compile error: exception NumberFormatException has already been caught
  • compiler নিজে থেকে ক্রম ঠিক করে নেয়

Java-র নিচের কোনটি checked exception?

  • NullPointerException
  • IOException
  • ArithmeticException
  • IllegalStateException

static int f() { try { return 1; } finally { return 2; } } — Java-তে f() কী ফেরত দেয়?

  • 1
  • compile error: finally-তে return লেখা যায় না
  • চলার সময় IllegalStateException
  • 2

try (var a = new A(); var b = new B()) { ... } — দুটিই AutoCloseable. block শেষে কোন ক্রমে close() ডাকা হয়?

  • আগে b, তারপর a
  • আগে a, তারপর b
  • ক্রম নিশ্চিত নয়, JVM ঠিক করে
  • কোনোটিই নিজে থেকে বন্ধ হয় না, finally-তে ডাকতে হয়

Java-তে int a = 10, b = 0; System.out.println(a / b); চালালে কী হয়?

  • Infinity ছাপা হয়
  • compile error
  • ArithmeticException: / by zero
  • 0 ছাপা হয়

try-with-resources-এর body একটি exception ছুড়ল, তারপর একটি resource-এর close()-ও exception ছুড়ল। catch কোনটি পায়?

  • শুধু close()-এর exception, মূলটি হারিয়ে যায়
  • body-র মূল exception, close()-এরটি তার getSuppressed()-এ
  • দুটি exception আলাদা করে, catch দুবার চলে
  • শুধু মূলটি, close()-এর exception চিরতরে ফেলে দেওয়া হয়

শব্দকোষ

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

মনে রাখুন

  1. exception = “আর এগোতে পারছি না” বার্তা-object: ছোড়া হলে stack unwind হয়, যতক্ষণ না মিলে যাওয়া catch পাওয়া যায়; কেউ না ধরলে প্রোগ্রাম থামে আর stack trace ছাপে — ওপর থেকে পড়ুন, সবচেয়ে নিচের Caused by-তে আসল কারণ।
  2. RuntimeException আর Error unchecked, Exception-এর বাকি সব checked: checked হলে ধরুন বা throws-এ ঘোষণা করুন। সামলানো যায় এমন ব্যর্থতায় checked, প্রোগ্রামের ভুলে unchecked.
  3. catch-এ subclass আগে; multi-catch-এ সম্পর্কহীন টাইপ; finally সবসময় চলে — কিন্তু finally থেকে কখনো return নয়।
  4. ফাঁকা catch নয়, catch (Exception e) নয়: হয় সামলান, নয় নিজের ভাষার exception-এ cause সহ আবার ছুড়ুন। exception দিয়ে control flow নয় — stack trace রেকর্ড করার খরচ আছে।
  5. close করতে হয় এমন সবকিছু try-with-resources-এ: খোলার উল্টো ক্রমে বন্ধ, আর close-এর exception suppressed হয়ে মূলটির সাথে থাকে।

পরের পর্বে: মডিউল ৫ শুরু — Collections. প্রথমে List: ArrayList আর LinkedList ভেতরে কীভাবে কাজ করে, কখন কোনটা, আর এই পর্বে দেখা IndexOutOfBoundsException আর UnsupportedOperationException কোন অবস্থায় আসে।

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

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