এই পর্বে যা শিখবেন
- Static typing কী, আর primitive ও reference type মেমরিতে কীভাবে আলাদা থাকে
- ভ্যারিয়েবল ঘোষণা, নামকরণের নিয়ম,
finalconstant আর “might not have been initialized” error - ৮টি primitive-এর আকার ও সীমা, literal লেখার নিয়ম, overflow আর দশমিকের সূক্ষ্মতার ফাঁদ
- Widening ও narrowing cast —
(byte) 200কেন −56 হয়, bit ধরে বোঝা - Wrapper class, autoboxing, Integer cache-এর ফাঁদ,
var-এর সঠিক ব্যবহার, আর AI-এর লেখা কোডে টাইপের ভুল ধরা
১ · Java আগে থেকেই টাইপ জানতে চায় — static typing
প্রোগ্রাম মানে ডেটা নিয়ে কাজ। কোনো শিক্ষার্থীর রোল নম্বর, তার GPA, মোবাইলের ব্যালান্স, একটি শহরের নাম — প্রতিটি একেকটি মান। এই মানগুলো মনে রাখতে প্রোগ্রাম মেমরিতে নাম দেওয়া ঘর ব্যবহার করে। এমন নাম দেওয়া ঘরকে বলে variable বা ভ্যারিয়েবল।
Java-তে প্রতিটি ভ্যারিয়েবলের সাথে একটি টাইপ বাঁধা থাকে — ঘরটিতে কোন ধরনের মান রাখা যাবে। int roll লিখলে roll ঘরে শুধু পূর্ণসংখ্যা থাকবে, চিরকাল। কাল সেখানে “রহিম” লেখা রাখতে চাইলে Java রাজি হবে না। আর এই নিয়ম যাচাই হয় প্রোগ্রাম চলার আগেই, javac যখন compile করে। একে বলে static typing.
বোর্ড পরীক্ষার OMR শিটের কথা ভাবুন। রোল নম্বরের ঘরে শুধু অঙ্ক ভরাট করা যায়, নামের ঘরে অক্ষর, আর বিষয় কোডের ঘরে নির্দিষ্ট সংখ্যক অঙ্ক। শিট জমা দেওয়ার আগেই পরিদর্শক দেখে নেন সব ঘর ঠিকমতো ভরা হয়েছে কি না। ভুল ঘর ভরাট থাকলে তখনই ধরা পড়ে — ফল প্রকাশের দিন নয়। Java-র compiler সেই পরিদর্শক: প্রোগ্রাম চালানোর আগেই সব ঘরের টাইপ মিলিয়ে দেখে।
Python-এ ভ্যারিয়েবলের কোনো স্থায়ী টাইপ নেই; একই নামে আজ সংখ্যা, কাল লেখা রাখা যায়, আর ভুল ধরা পড়ে চলার সময় — একে বলে dynamic typing. Java ইচ্ছা করে কঠোর পথ বেছে নিয়েছে। লাভ তিনটি: ভুল আগে ধরা পড়ে; IDE জানে কোন ভ্যারিয়েবলে কী আছে, তাই সঠিক পরামর্শ দিতে পারে; আর পর্ব ০৫-এ দেখা iadd-এর মতো নির্দেশ চলার সময় টাইপ খুঁজে সময় নষ্ট করে না। দাম হলো একটু বেশি লেখা — সেটাও var অনেকটা কমিয়েছে (অংশ ৭)।
দুই পরিবারের টাইপ
Java-র সব টাইপ দুটি পরিবারে ভাগ করা। এই ভাগটি বুঝলে পুরো পর্ব — আসলে পুরো কোর্স — সহজ হয়ে যায়:
📦 Primitive type — ৮টি
byte, short, int, long, float, double, char, boolean. ঘরের ভেতরে মানটাই থাকে। নাম ছোট হাতের অক্ষরে, আর এগুলো object নয়।
🔗 Reference type — বাকি সব
String, Integer, array, আপনার নিজের class … ঘরে থাকে heap-এর একটি object-এর খোঁজ (reference), object-টি নিজে নয়। নাম সাধারণত বড় হাতের অক্ষরে শুরু।
পর্ব ০৫-এ stack আর heap-এর ছবি দেখেছেন। এবার সেই ছবিতে টাইপ বসিয়ে দেখি। ছয়টি লাইন — প্রতিটি লাইনের পর stack-এর ঘর আর heap-এর object কীভাবে বদলায়, খেয়াল করুন:
1int a = 5;2double gpa = 3.75;3int b = a;4String s = "ঢাকা";5String t = s;6b = 9;
| stack: a (int) | |
| stack: gpa (double) | |
| stack: b (int) | |
| stack: s (String) | |
| stack: t (String) | |
| heap |
int a = 5;— stack-এ ৪ byte-এর একটি ঘর তৈরি হলো, নামa. ঘরের ভেতরে সরাসরি 5-এর bit-গুলো। কোথাও কোনো object নেই।double gpa = 3.75;—double-এর ঘর ৮ byte-এর। টাইপই ঠিক করে দেয় ঘর কত বড় হবে, আর তার bit-গুলো কীভাবে পড়তে হবে।int b = a;—a-এর ভেতরের মান, অর্থাৎ 5, কপি হলোb-তে। এখন দুটি আলাদা ঘরে দুটি আলাদা 5. একটির সাথে আরেকটির আর কোনো সম্পর্ক নেই।String s = "ঢাকা";— এবার দুটি জিনিস। heap-এ একটিStringobject, যাতে লেখা “ঢাকা”। আর stack-এরsঘরে শুধু সেই object-টির reference — কোথায় আছে তার খোঁজ।String t = s;—s-এর ভেতরে যা আছে, সেটাই কপি হলো — অর্থাৎ খোঁজটি। নতুন কোনো String তৈরি হয়নি।sআরtদুজনেই একই object দেখায়।b = 9;—b-এর ঘরে নতুন মান বসল।aএখনো 5, কারণ লাইন ৩-এ মানটাই কপি হয়েছিল। নিয়ম একটাই: Java সবসময় ঘরের ভেতরের জিনিস কপি করে। Primitive-এর ঘরে মান থাকে, তাই মান কপি হয়; reference-এর ঘরে খোঁজ থাকে, তাই খোঁজ কপি হয়।
টাইপ দুটি কথা বলে দেয়: ঘর কত বড়, আর ঘরের bit-গুলোর মানে কী। একই ৩২টি bit int হিসেবে পড়লে একটি পূর্ণসংখ্যা, float হিসেবে পড়লে সম্পূর্ণ অন্য একটি দশমিক সংখ্যা। Compiler টাইপ জানে বলেই ঠিক নির্দেশ বেছে নিতে পারে, আর ভুল মিশ্রণ আটকাতে পারে।
২ · ভ্যারিয়েবল: ঘোষণা, নাম আর প্রথম মান
একটি ভ্যারিয়েবল তৈরি করাকে বলে declaration বা ঘোষণা — টাইপ, তারপর নাম। প্রথমবার মান বসানোকে বলে initialization. দুটো এক লাইনে করা যায়, আলাদাও করা যায়:
int roll; // ঘোষণা: int টাইপের ঘর, নাম roll
roll = 42; // initialization: প্রথম মান
double gpa = 4.75; // ঘোষণা আর initialization একসাথে
int math = 85, physics = 78; // একই টাইপের একাধিক ভ্যারিয়েবল
final int MAX_MARKS = 100; // final: একবার মান বসলে আর বদলানো যাবে নানামকরণের নিয়ম আর রীতি
কিছু নিয়ম compiler জোর করে মানায়, আর কিছু রীতি পুরো Java জগৎ মেনে চলে। রীতি না মানলেও কোড চলবে, কিন্তু অন্য প্রোগ্রামার — আর ইন্টারভিউয়ার — সঙ্গে সঙ্গে খেয়াল করবেন:
| ধরন | কী | উদাহরণ |
|---|---|---|
| নিয়ম (না মানলে compile error) | অক্ষর, অঙ্ক, _ আর $ দিয়ে নাম; অঙ্ক দিয়ে শুরু নয়; keyword নয়; ছোট-বড় হাতের অক্ষর আলাদা | 2ndRoll ✘, class ✘, Roll আর roll দুটি আলাদা নাম |
| রীতি: ভ্যারিয়েবল ও method | camelCase — প্রথম শব্দ ছোট হাতে, পরের প্রতিটি শব্দের প্রথম অক্ষর বড় | studentName, totalMarks, balancePoisha |
| রীতি: constant | static final বা final মান — সব বড় হাতে, শব্দের মাঝে _ (UPPER_SNAKE_CASE) | MAX_MARKS, VAT_RATE |
| রীতি: class | PascalCase — প্রতিটি শব্দের প্রথম অক্ষর বড় | Student, BankAccount |
অর্থপূর্ণ নাম দিন। x, temp2, data ছয় মাস পরে আপনাকে কিছুই বলবে না; monthlyFee বলবে। একটি মজার তথ্য: Java-র নামে Unicode অক্ষর চলে, তাই int বয়স = 20; আসলেই compile হয়। তবে বাস্তব প্রজেক্টে সবাই ইংরেজি নাম ব্যবহার করে — সব কি-বোর্ডে, সব টুলে, সব সহকর্মীর কাছে তা সহজ। আর Java 9 থেকে শুধু একটি _ একা নাম হিসেবে ব্যবহার করা যায় না।
final লেখা ভ্যারিয়েবলে দ্বিতীয়বার মান বসাতে গেলে compiler থামিয়ে দেয়: cannot assign a value to final variable MAX_MARKS. যে মান বদলানোর কথা নয়, তাতে final দিলে ভুল করে বদলে ফেলার পথ বন্ধ হয়। static final দিয়ে class-স্তরের constant বানানো পর্ব ১৮-এ বিস্তারিত দেখবেন।
মান না দিয়ে ব্যবহার — definite assignment
C-তে মান না বসানো local ভ্যারিয়েবল পড়লে পাওয়া যায় মেমরিতে আগে থেকে পড়ে থাকা আবর্জনা — প্রতিবার ভিন্ন, আর ভীষণ বিপজ্জনক। Java এই সম্ভাবনাটিই বন্ধ করে দিয়েছে। কোনো local variable পড়ার আগে compiler প্রমাণ করতে চায় যে প্রতিটি সম্ভাব্য পথে তাতে অন্তত একবার মান বসেছে। এই নিয়মের নাম definite assignment. নিচের কোডে marks-এ মান বসে শুধু একটি শর্ত সত্য হলে:
int marks;
if (args.length > 5) marks = 80;
System.out.println(marks); // শর্ত মিথ্যা হলে marks-এ কী আছে?Main.java:5: error: variable marks might not have been initialized
System.out.println(marks);
^
1 error
লক্ষ করুন, compiler জানে না প্রোগ্রাম চলার সময় শর্তটি সত্য হবে কি না। সে শুধু দেখে যে এমন একটি পথ আছে যেখানে মান বসে না — আর সেটাই যথেষ্ট। সমাধান সহজ: ঘোষণার সময়েই একটি অর্থপূর্ণ প্রথম মান দিন, বা else শাখাতেও মান বসান। এই নিয়মটি শুধু local ভ্যারিয়েবলের জন্য; class-এর field-এর ক্ষেত্রে আলাদা নিয়ম, যা অংশ ৮-এ দেখবেন।
প্রতিটি ভ্যারিয়েবলের একটি এলাকা আছে, যাকে বলে scope. যে { } block-এর ভেতরে ঘোষণা হয়েছে, ভ্যারিয়েবলটি শুধু সেই block-এর ভেতরেই বেঁচে থাকে। block শেষ হলে নামটি আর ব্যবহার করা যায় না, আর ঘরটিও stack থেকে সরে যায়। লুপ আর method-এর পর্বে (পর্ব ১১, ১২) এটি আরও কাজে লাগবে।
৩ · আটটি primitive — কোনটিতে কত বড় মান ধরে
Java-তে ঠিক ৮টি primitive type. C-র মতো “মেশিনভেদে int ২ বা ৪ byte” — এমন অনিশ্চয়তা এখানে নেই। Java Language Specification প্রতিটির আকার নির্দিষ্ট করে দিয়েছে, তাই আপনার ল্যাপটপে, সার্ভারে আর ফোনে int সবসময় ৩২ bit. Portability-র প্রতিশ্রুতির এটি একটি বড় অংশ।
| টাইপ | আকার | সীমা (মোটামুটি) | কখন ব্যবহার করবেন |
|---|---|---|---|
byte | ৮ bit | −128 থেকে 127 | ফাইল বা নেটওয়ার্কের কাঁচা ডেটা |
short | ১৬ bit | −32,768 থেকে 32,767 | খুব কম ক্ষেত্রে; বড় array-তে মেমরি বাঁচাতে |
int | ৩২ bit | প্রায় ±২১৪ কোটি (±2,147,483,647) | পূর্ণসংখ্যার ডিফল্ট পছন্দ: রোল, নম্বর, গণনা |
long | ৬৪ bit | প্রায় ±9.22 × 1018 | বিশাল সংখ্যা: পয়সায় টাকা, সময় (মিলিসেকেন্ড), বিশ্বের জনসংখ্যা |
float | ৩২ bit | প্রায় ±3.4 × 1038, নির্ভুল অঙ্ক ~৭টি | গ্রাফিক্স, বিশাল সেন্সর-ডেটা; সাধারণ কাজে কম |
double | ৬৪ bit | প্রায় ±1.8 × 10308, নির্ভুল অঙ্ক ~১৫–১৬টি | দশমিকের ডিফল্ট পছন্দ: GPA, দূরত্ব, বিজ্ঞানের হিসাব |
char | ১৬ bit | 0 থেকে 65,535 (চিহ্ন ছাড়া) | একটি অক্ষর: 'A', 'অ' |
boolean | নির্দিষ্ট নয় | true বা false | হ্যাঁ/না: পাস করেছে কি না |
প্রথম চারটি পূর্ণসংখ্যার টাইপ, সবই চিহ্নযুক্ত (ঋণাত্মক হতে পারে)। এরা ঋণাত্মক সংখ্যা রাখে two's complement পদ্ধতিতে — সবচেয়ে বাঁয়ের bit 1 হলে সংখ্যাটি ঋণাত্মক। তাই ৮ bit-এ ধনাত্মক দিকে 127 পর্যন্ত, ঋণাত্মক দিকে −128 পর্যন্ত। এই তথ্যটি অংশ ৫-এ (byte) 200-এর রহস্য খুলবে। boolean-এর আকার JVM-এর ওপর ছেড়ে দেওয়া; তবে boolean-কে সংখ্যা হিসেবে ব্যবহার করা যায় না — C-র মতো if (1) Java-তে চলে না।
char — একটি UTF-16 ঘর
char ১৬ bit-এর একটি চিহ্নহীন সংখ্যা, যা একটি Unicode অক্ষরের কোড রাখে। 'অ'-এর কোড 2437 (hex-এ 0985). বাংলার সব মৌলিক অক্ষর, কার আর ফলা Unicode-এর প্রথম ৬৫,৫৩৬টি কোডের মধ্যে পড়ে, তাই প্রতিটি এক char-এ ধরে। তবে সাবধান: “ক্ষ” দেখতে একটি অক্ষর হলেও ভেতরে তিনটি কোড — ক, হসন্ত, ষ — অর্থাৎ তিনটি char. আর 😀-এর মতো অনেক ইমোজি ও কিছু বিরল লিপির অক্ষর ১৬ bit-এ ধরে না; তাদের লাগে দুটি char, যাকে বলে surrogate pair. তাই সঠিক কথা হলো: char একটি UTF-16 code unit, সবসময় একটি পূর্ণ অক্ষর নয়। কেন এমন হলো, তার ইতিহাস ভাবনার প্রশ্ন ৩-এ।
Literal — কোডে সরাসরি লেখা মান
কোডে সরাসরি লেখা 42, 3.5, 'A', true — এদের বলে literal. প্রতিটি literal-এরও একটি টাইপ আছে, আর সেটা না জানলে অদ্ভুত error আসে:
| লেখা | টাইপ ও মানে |
|---|---|
42 | ডিফল্টভাবে int |
10L | long (ছোট l চলে, কিন্তু 1-এর মতো দেখায় — বড় L লিখুন) |
1_000_000 | পড়ার সুবিধার জন্য _ — মান 1000000 (Java 7 থেকে) |
0x1F · 0b1010 | hex (31) · binary (10). সাবধান: 017 মানে octal, অর্থাৎ 15! |
3.5 | ডিফল্টভাবে double |
3.5f | float — f না দিলে float x = 3.5; compile হয় না |
'অ' · '\n' | char — একক উদ্ধৃতিচিহ্ন; "অ" লিখলে সেটা String |
সবচেয়ে প্রচলিত ভুলটি দেখুন। বিশ্বের জনসংখ্যা প্রায় ৮০০ কোটি, যা int-এর সীমার অনেক বাইরে। কেউ লিখলেন long world = 8000000000; — long দিয়েছেন, তবু error:
Main.java:3: error: integer number too large
long world = 8000000000;
^
কারণ ডান দিকের literal নিজেই int, আর int হিসেবে এত বড় সংখ্যা লেখাই যায় না। শেষে L দিলে সমাধান: 8_000_000_000L. অন্যদিকে বাংলাদেশের জনসংখ্যা (প্রায় ১৭ কোটি) int-এ আরামে ধরে — টাইপ বাছাই মানে সম্ভাব্য সবচেয়ে বড় মানটি নিয়ে আগে ভাবা।
এবার একটি শিক্ষার্থীর রেকর্ড, মোবাইল রিচার্জের ব্যালান্স আর কয়েকটি literal একসাথে চালিয়ে দেখুন:
class Main {
public static void main(String[] args) {
// একজন শিক্ষার্থীর রেকর্ড — প্রতিটি তথ্যের জন্য মানানসই টাইপ
int roll = 42;
double gpa = 4.75;
char grade = 'A';
boolean passed = true;
System.out.println("রোল " + roll + ", GPA " + gpa + ", গ্রেড " + grade + ", পাস " + passed);
long balancePoisha = 15_050L; // ১৫০ টাকা ৫০ পয়সা, পয়সায় রাখা
long worldPopulation = 8_000_000_000L; // int-এ ধরে না
System.out.println("ব্যালান্স: " + balancePoisha / 100 + " টাকা " + balancePoisha % 100 + " পয়সা");
System.out.println("বিশ্বের জনসংখ্যা প্রায় " + worldPopulation);
System.out.println(0x1F + " " + 0b1010 + " " + 1_000_000);
char bn = 'অ';
System.out.println(bn + " অক্ষরের কোড " + (int) bn);
System.out.println("int-এর সর্বোচ্চ " + Integer.MAX_VALUE);
System.out.println("long-এর সর্বোচ্চ " + Long.MAX_VALUE);
}
}আউটপুটের প্রথম লাইন রোল 42, GPA 4.75, গ্রেড A, পাস true. ব্যালান্সের লাইনে আসে 150 টাকা 50 পয়সা — পয়সায় রাখা পূর্ণসংখ্যাকে 100 দিয়ে ভাগ করলে টাকা, আর ভাগশেষ (%) পয়সা। এখানে / পূর্ণসংখ্যার ভাগ, তাই দশমিক আসে না (অংশ ৫ ও পর্ব ০৭)। literal-এর লাইনে 31 10 1000000 — hex আর binary শুধু লেখার ধরন, ভেতরে একই সংখ্যা। (int) bn অক্ষরটির কোড দেখায়: অ অক্ষরের কোড 2437. শেষ দুই লাইনে 2147483647 আর 9223372036854775807 — এই দুটি সংখ্যার মোটামুটি আকার মনে রাখা কাজের।
৪ · দুটি নীরব ফাঁদ — overflow আর দশমিকের সূক্ষ্মতা
প্রতিটি টাইপের ঘর নির্দিষ্ট আকারের। প্রশ্ন হলো, ঘরের সর্বোচ্চ মানের চেয়ে বড় কিছু ঢোকালে কী হয়? অনেকে ভাবেন Java একটি error দেবে। দেয় না। পূর্ণসংখ্যা চুপচাপ উল্টো প্রান্তে ঘুরে যায় — একে বলে integer overflow.
পুরোনো মোটরসাইকেলের ওডোমিটারে পাঁচ ঘর থাকলে 99999 কিলোমিটারের পর আরেক কিলোমিটার চালালে মিটার দেখায় 00000. বাইক নতুন হয়নি, মিটারের ঘর ফুরিয়ে গেছে। int-এর ঘরও তেমন — শুধু পার্থক্য হলো ঘোরার পর সে শূন্যে নয়, সবচেয়ে ছোট ঋণাত্মক সংখ্যায় পৌঁছায়।
1int x = 2147483646;2x = x + 1;3x = x + 1;4x = x + 1;
| x (দশমিকে) | |
| x (hex) | |
| সবচেয়ে বাঁয়ের bit |
- শুরু।
x-এ রাখা হলোint-এর সর্বোচ্চ মানের চেয়ে ১ কম। hex-এ7FFFFFFE— প্রথম bit 0, মানে ধনাত্মক। - প্রথম যোগ। এখন
xঠিকInteger.MAX_VALUE, অর্থাৎ 2147483647. hex-এ7FFFFFFF: প্রথম bit 0, বাকি ৩১টি bit সব 1. এর চেয়ে বড় ধনাত্মক সংখ্যা ৩২ bit-এ রাখা যায় না। - দ্বিতীয় যোগ — overflow. CPU সাধারণ binary যোগ করল:
7FFFFFFF + 1 = 80000000. কিন্তু এখন প্রথম bit 1, আর two's complement-এ তার মানে ঋণাত্মক। ফল: −2147483648. কোনো exception নেই, কোনো সতর্কবার্তা নেই। - তৃতীয় যোগ। প্রোগ্রাম কিছুই টের পায়নি; ভুল মান নিয়েই হিসাব চলছে। ব্যাংকের হিসাবে বা গেমের স্কোরে এমন ভুল দিনের পর দিন চোখের আড়ালে থাকতে পারে। Java এখানে C থেকে একটু ভালো — C-তে signed overflow “undefined behavior”, Java-তে ফল অন্তত সবসময় নির্দিষ্ট — কিন্তু ভুল ফল তো ভুলই।
Overflow এড়াতে দুটি অস্ত্র। প্রথমত, মান বড় হতে পারলে শুরু থেকেই long নিন। দ্বিতীয়ত, যেখানে overflow হলে চুপচাপ ভুল ফলের চেয়ে প্রোগ্রাম থেমে যাওয়া ভালো (টাকা, স্টক, গণনা), সেখানে Math.addExact, Math.multiplyExact বা Math.toIntExact ব্যবহার করুন — সীমা ছাড়ালে এরা ArithmeticException ছুড়ে দেয় (Java 8 থেকে)। একটি সূক্ষ্ম ফাঁদও মনে রাখুন: long ms = 24 * 60 * 60 * 1000 * 1000; লিখলে বাঁ পাশে long থাকলেও ডান পাশের পুরো গুণ হয় int-এ, overflow হয় সেখানেই, তারপর ভুল ফলটি long-এ যায়। সমাধান: প্রথম সংখ্যাটিকেই 24L লিখুন।
দশমিক কেন পুরোপুরি নির্ভুল নয়
float আর double দশমিক সংখ্যা রাখে binary ভগ্নাংশে, IEEE 754 নামের আন্তর্জাতিক মান অনুযায়ী — একে বলে floating-point. সমস্যা হলো, দশভিত্তিক অনেক সহজ ভগ্নাংশ binary-তে শেষ হয় না। যেমন ১/৩ দশমিকে 0.3333… চলতেই থাকে, তেমনি 0.1 binary-তে চলতেই থাকে। ঘর সীমিত, তাই কোথাও কেটে ফেলতে হয়। ফল:
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(1.1 * 3); // 3.3000000000000003
বিজ্ঞানের হিসাবে এই ক্ষুদ্র ভুল কোনো সমস্যা নয়। কিন্তু টাকার হিসাবে লাখো লেনদেনে জমতে জমতে পয়সার গরমিল হয়, আর হিসাবরক্ষক তা মেনে নেবেন না। টাকার জন্য দুটি সঠিক পথ:
- পয়সায়
long: ১৫০.৫০ টাকা = 15050 পয়সা। সব হিসাব পূর্ণসংখ্যায়, তাই কোনো কাটাকাটি নেই। মোবাইল ব্যাংকিং বা রিচার্জের মতো একক মুদ্রার সরল হিসাবে চমৎকার। - BigDecimal: দশভিত্তিক নির্ভুল দশমিক, যত অঙ্ক দরকার। ভ্যাট, সুদ, ভাগ আর রাউন্ডিংয়ের নিয়ম থাকলে এটাই মানক পছন্দ। তবে বানাতে হবে String থেকে:
new BigDecimal("0.1")ঠিক 0.1. আরnew BigDecimal(0.1)বানায় double-এর ভেতরের আসল মানটি —0.1000000000000000055511151231257827…— অর্থাৎ ভুলটি সাথে করেই নিয়ে আসে।
দশমিক সংখ্যা নিরাপদে তুলনা করা আর রাউন্ডিংয়ের নিয়ম পর্ব ০৭-এ দেখবেন, অপারেটরের সাথে।
৫ · টাইপ বদল — widening আর narrowing cast
এক টাইপের মান আরেক টাইপের ঘরে রাখতে চাইলে দুটি পরিস্থিতি হয়। ছোট বাক্সের জিনিস বড় বাক্সে রাখলে কিছু হারানোর ভয় নেই, তাই Java নিজেই করে দেয় — একে বলে widening conversion. ক্রমটি এরকম:
char → int
উল্টো দিকে, বড় বাক্সের জিনিস ছোট বাক্সে ঢোকাতে গেলে কিছু কেটে যেতে পারে। Java তাই নিজে থেকে করে না: int x = 3.99; লিখলে compile error — incompatible types: possible lossy conversion from double to int. আপনাকে স্পষ্ট করে বলতে হয়, “জানি কিছু হারাতে পারে, তবু করো”। এই স্পষ্ট নির্দেশের নাম cast, আর এই রূপান্তরকে বলে narrowing conversion. লেখার নিয়ম: মানের আগে বন্ধনীতে নতুন টাইপ — (int) 3.99.
Narrowing-এ ঠিক কী হারায়? দুটি আলাদা নিয়ম। দশমিক থেকে পূর্ণসংখ্যায় গেলে দশমিক অংশ কেটে ফেলা হয় — রাউন্ড নয়: (int) 3.99 হয় 3, (int) -3.99 হয় −3. আর বড় পূর্ণসংখ্যা থেকে ছোটতে গেলে শুধু ডান দিকের bit-গুলো রাখা হয়, বাকিগুলো ফেলে দেওয়া হয়। দ্বিতীয় নিয়মটিই (byte) 200-এর রহস্য। ধাপে ধাপে দেখুন:
1byte b = 10;2int i = b; // widening3double d = i; // widening4int cut = (int) 3.99; // narrowing5int big = 200;6byte nb = (byte) big; // narrowing
| ঘর ও আকার | |
| bit (শেষের অংশ) | |
| মান |
byte b = 10;— ৮ bit-এর ঘরে 10, binary-তে00001010. 10 সংখ্যাটিintliteral, তবু চলে — ছোট constantbyte-এর সীমার মধ্যে থাকলে compiler নিজে মানিয়ে নেয়।int i = b;— widening. ৮ bit থেকে ৩২ bit-এ ওঠা। বাঁ দিকে শূন্য (ঋণাত্মক হলে 1) বসিয়ে ঘর ভরানো হয়, মান হুবহু থাকে। কোনো cast লাগেনি, কারণ কিছু হারানোর সম্ভাবনা নেই।double d = i;— widening. পূর্ণসংখ্যা থেকে দশমিকে। মান 10.0. একটি সূক্ষ্ম ব্যতিক্রম জেনে রাখুন:int→floatবাlong→doublewidening হলেও খুব বড় সংখ্যায় শেষের দিকের অঙ্ক হারাতে পারে —float f = 16_777_217;ছাপে1.6777216E7. Java এটিকে cast ছাড়াই করতে দেয়, তাই এখানে চোখ রাখতে হয় আপনাকেই।(int) 3.99— দশমিক কাটা। ফল 3, 4 নয়। Java দশমিক অংশ ফেলে দেয়, শূন্যের দিকে। রাউন্ড চাইলেMath.round(3.99)— যা 4 দেয় (তবেlongটাইপে)।int big = 200;— ৩২ bit-এ 200: শেষ ৮ bit11001000, বাকি সব শূন্য।int-এ এটি নিরীহ ধনাত্মক সংখ্যা।(byte) big— শুধু শেষ ৮ bit. ওপরের ২৪টি শূন্য ফেলে দেওয়া হলো, রইল11001000. কিন্তুbyte-এ সবচেয়ে বাঁয়ের bit 1 মানে ঋণাত্মক! two's complement-এ এর মান 200 − 256 = −56. কোনো error নেই — আপনি cast লিখে বলেছিলেন “আমি জানি”। Cast হলো compiler-কে দেওয়া প্রতিশ্রুতি; প্রতিশ্রুতি ভুল হলে ফলও ভুল।
পূর্ণসংখ্যার ভাগ আর টাইপের উন্নীতকরণ
টাইপের আরেকটি নিয়ম প্রতিদিন কাজে লাগে: অপারেটরের দুই পাশই int হলে ফলও int. তাই 7 / 2 হয় 3 — দশমিক অংশ হারায়। একে বলে integer division. যেকোনো এক পাশ double হলে অন্য পাশও widening হয়ে double, আর ফল 3.5: 7 / 2.0 বা (double) 7 / 2. গড় নম্বর বের করতে গিয়ে total / count লিখে দশমিক হারানো খুব প্রচলিত ভুল।
আরেকটি চমক: byte, short বা char নিয়ে হিসাব করলে Java আগে সবাইকে int-এ তুলে নেয়। তাই short s = 10; s = s + 5; compile হয় না (possible lossy conversion from int to short), কারণ s + 5-এর টাইপ int. অথচ s += 5; চলে — যৌগিক assignment ভেতরে নিজেই cast করে নেয়। অপারেটরের পুরো নিয়ম পর্ব ০৭-এ।
৬ · Wrapper class, autoboxing আর Integer cache-এর ফাঁদ
Primitive দ্রুত আর ছোট, কিন্তু object নয়। Java-র অনেক জায়গায় আবার শুধু object চলে — যেমন ArrayList-এর মতো collection (পর্ব ৩০) বা generics (পর্ব ২৮)। ArrayList<int> লেখা যায় না। এই ফাঁক ভরাতে প্রতিটি primitive-এর জন্য একটি করে class আছে, যা মানটিকে একটি object-এর ভেতরে মুড়ে রাখে — wrapper class:
| Primitive | Wrapper | কাজের কিছু জিনিস |
|---|---|---|
int | Integer | Integer.parseInt("42"), Integer.MAX_VALUE |
long | Long | Long.parseLong(...) |
double | Double | Double.parseDouble("4.75") |
char | Character | Character.isDigit(c), Character.isLetter(c) |
boolean | Boolean | Boolean.parseBoolean("true") |
byte, short, float | Byte, Short, Float | একই ধরনের |
Java 5-এর আগে primitive আর wrapper-এর মধ্যে আসা-যাওয়া হাতে লিখতে হতো। এখন compiler নিজেই করে। Integer n = 42; লিখলে compiler আসলে লেখে Integer n = Integer.valueOf(42); — একে বলে autoboxing. উল্টো দিকে int k = n; হয়ে যায় int k = n.intValue(); — একে বলে unboxing. সুবিধাজনক, কিন্তু এই অদৃশ্য কোড থেকেই দুটি বিখ্যাত ফাঁদের জন্ম।
পুরোনো বই বা AI অনেক সময় new Integer(5) লেখে। এই constructor Java 9 থেকে deprecated, আর নতুন JDK-তে compile করলে সতর্কবার্তা দেয় — ভবিষ্যতে মুছে ফেলার জন্য চিহ্নিত। লিখুন Integer.valueOf(5), বা সরাসরি Integer n = 5;
ফাঁদ ১: == দিয়ে Integer তুলনা
পর্ব ০৫-এ দেখেছেন, object-এর ক্ষেত্রে == জিজ্ঞেস করে “দুটি reference কি একই object দেখায়?” — মান সমান কি না নয়। Integer একটি object. তাহলে দুটি Integer-এ একই সংখ্যা থাকলে == কী বলবে? উত্তরটি সংখ্যার ওপর নির্ভর করে — আর ঠিক এখানেই ফাঁদ:
1Integer a = 127;2Integer b = 127;3System.out.println(a == b);4Integer c = 128;5Integer d = 128;6System.out.println(c == d);7System.out.println(c.equals(d));
| a | |
| b | |
| c | |
| d |
Integer a = 127;— compiler লিখে নেয়Integer.valueOf(127).valueOf−128 থেকে 127 পর্যন্ত প্রতিটি সংখ্যার জন্য আগে থেকে বানানো একটি করে object রাখে — একটি তাক, যাকে বলে Integer cache. 127 সেই সীমার ভেতরে, তাই নতুন object নয়, তাক থেকে দেওয়া হলো।Integer b = 127;— আবারvalueOf(127), আবার সেই একই তাকের একই object. এখনaআরbদুজনেই একটি object দেখায়।a == b→true. দুটি reference একই object দেখায়, তাই সত্য। দেখে মনে হয় “মান সমান, তাই true” — কিন্তু আসল কারণ cache. এই কাকতালীয় সাফল্যই ফাঁদটিকে বিপজ্জনক করে।Integer c = 128;— 128 cache-এর সীমার বাইরে।valueOfheap-এ একটি নতুনIntegerobject বানাল (ধরুন X).Integer d = 128;— আবার সীমার বাইরে, আবার নতুন object (Y). দুটিতেই 128, কিন্তু heap-এ এরা দুটি আলাদা object.c == d→false. X আর Y আলাদা object, তাই==মিথ্যা — মান এক হওয়া সত্ত্বেও। ছোট সংখ্যা দিয়ে পরীক্ষা করা কোড পাস করে, আর আসল ডেটায় (রোল ২০০, দাম ৫০০) চুপচাপ ভুল উত্তর দেয়।c.equals(d)→true.equalsভেতরের মান তুলনা করে। নিয়ম: wrapper object তুলনায় সবসময়equals— বা আগে unbox করেint-এ তুলনা। cache-এর ঊর্ধ্বসীমা JVM flag দিয়ে বাড়ানো যায়, কিন্তু Java-র নিয়ম শুধু −128 থেকে 127-এর নিশ্চয়তা দেয়; সেই ভরসায় কোড লেখা চলে না।
ফাঁদ ২: null unbox করা
Primitive কখনো null হতে পারে না, কিন্তু wrapper পারে — সেটা object-এর reference. তাই Integer-কে int-এ unbox করার সময় ভেতরে থাকা null-এর ওপর intValue() ডাকা হয়, আর প্রোগ্রাম NullPointerException-এ থেমে যায়:
Integer bonus = null; // যেমন: ডেটাবেসে বোনাসের ঘর ফাঁকা
int total = 5000 + bonus; // unboxing → bonus.intValue()
Exception in thread "main" java.lang.NullPointerException: Cannot invoke
"java.lang.Integer.intValue()" because "<local1>" is null
লাইনটিতে কোথাও . দিয়ে method ডাকা দেখা যায় না, তবু NullPointerException — কারণ ডাকাটা compiler লুকিয়ে বসিয়েছে। Java 14-এ আসা আর Java 15 থেকে ডিফল্টভাবে চালু এই বার্তা (helpful NullPointerException) বলে দেয় ঠিক কোন method ডাকতে গিয়ে সমস্যা হয়েছে। (<local1>-এর জায়গায় ভ্যারিয়েবলের আসল নাম দেখতে কোড debug তথ্যসহ — javac -g — compile করতে হয়; IDE সাধারণত তা-ই করে।) Map থেকে মান আনা, ডেটাবেস বা JSON থেকে আসা সংখ্যা — যেখানেই wrapper আছে, সেখানে null-এর সম্ভাবনা ভাবুন।
একটি তৃতীয়, কম চোখে পড়া দামও আছে: গতি। Long sum = 0L; দিয়ে লুপে কোটিবার sum += i করলে প্রায় প্রতিবার একটি নতুন Long object তৈরি হয়, আর garbage collector-কে সেগুলো পরিষ্কার করতে হয়। long sum লিখলে একই কাজ অনেক দ্রুত। নিয়ম: হিসাবের ভ্যারিয়েবলে primitive; collection বা null-এর দরকার হলে তবেই wrapper.
এবার এই পর্বের ফাঁদগুলো — Integer cache, overflow আর দশমিক — নিজে চালিয়ে দেখুন:
import java.math.BigDecimal;
class Main {
public static void main(String[] args) {
Integer a = 127, b = 127, c = 128, d = 128;
System.out.println("127: == " + (a == b) + ", equals " + a.equals(b));
System.out.println("128: == " + (c == d) + ", equals " + c.equals(d));
int max = Integer.MAX_VALUE;
System.out.println("max + 1 = " + (max + 1));
try {
Math.addExact(max, 1);
} catch (ArithmeticException e) {
System.out.println("addExact: " + e.getMessage());
}
System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));
BigDecimal sum = new BigDecimal("0.1").add(new BigDecimal("0.2"));
System.out.println("BigDecimal: " + sum);
System.out.println("(byte) 200 = " + (byte) 200 + ", (int) 3.99 = " + (int) 3.99);
}
}আউটপুটের প্রথম দুই লাইন: 127: == true, equals true আর 128: == false, equals true — == cache-এর সীমায় এসে আচরণ বদলায়, equals সবসময় মান তুলনা করে। এরপর max + 1 = -2147483648 — নীরব overflow. একই যোগ Math.addExact দিয়ে করলে exception আসে, যা আমরা ধরে নিয়ে বার্তাটি ছাপছি: addExact: integer overflow (try/catch-এর পুরো আলোচনা পর্ব ২৯-এ)। তারপর 0.1 + 0.2 = 0.30000000000000004, আর BigDecimal-এ ঠিক 0.3. শেষ লাইনে (byte) 200 = -56, (int) 3.99 = 3 — সিঁড়ির stepper-এ যা দেখেছেন। (নিজের প্রজেক্টে main class সাধারণত public class Main; এই পাতার রানারের জন্য public বাদ রাখা হয়েছে।)
C/C++-এ int, long-এর আকার প্ল্যাটফর্মভেদে বদলায় আর signed overflow undefined behavior; Java-তে আকার নির্দিষ্ট, overflow সবসময় নির্দিষ্টভাবে ঘুরে যায়, আর চিহ্নহীন (unsigned) টাইপ নেই — শুধু char চিহ্নহীন (C-এর ভ্যারিয়েবল ও টাইপ)। Python-এর int-এর কোনো সীমা নেই, তাই overflow-ই হয় না, আর সব মানই object (Python-এর ভ্যারিয়েবল ও টাইপ)। Java এই দুইয়ের মাঝামাঝি: দ্রুত, সীমিত primitive আর object-এর জগৎ পাশাপাশি, যাদের মধ্যে সেতু হলো wrapper.
৭ · var — টাইপ লিখতে হয় না, কিন্তু টাইপ থাকে
Java 10 (২০১৮) থেকে local ভ্যারিয়েবলের টাইপ না লিখে var লেখা যায়। Compiler ডান দিকের মান দেখে টাইপ নিজে বের করে নেয় — একে বলে type inference:
var roll = 42; // int
var gpa = 4.75; // double
var name = "করিম"; // String
var marks = new java.util.ArrayList<Integer>(); // ArrayList<Integer>
// আগে লিখতে হতো:
// java.util.ArrayList<Integer> marks = new java.util.ArrayList<Integer>();সবচেয়ে বড় ভুল ধারণাটি আগে ভেঙে দিই: var Java-কে dynamic বানায় না। var roll = 42;-এর পর roll চিরকাল int; সেখানে "রহিম" রাখতে গেলে আগের মতোই compile error. টাইপ বের করা হয় compile-এর সময়, একবারই, আর bytecode-এ কোনো পার্থক্য থাকে না — var শুধু আপনার লেখার শর্টকাট। এজন্যই কিছু জায়গায় var চলে না, কারণ compiler-এর কাছে অনুমানের কিছু থাকে না:
var y; // error: cannot infer type for local variable y
// (cannot use 'var' on variable without initializer)
var z = null; // error: cannot infer type for local variable z
// (variable initializer is 'null')
আর var শুধু local ভ্যারিয়েবলে (লুপের ভ্যারিয়েবলসহ) — class-এর field, method-এর parameter বা return type-এ নয়। মজার তথ্য: var আসলে keyword নয়, একটি “reserved type name” — তাই পুরোনো কোডে কারও ভ্যারিয়েবলের নাম var থাকলে সেটা ভাঙেনি।
✔ যেখানে var সাহায্য করে
ডান দিকেই টাইপ স্পষ্ট: var list = new ArrayList<String>();, var sc = new Scanner(System.in);. লম্বা generic টাইপ দুবার লেখার ঝামেলা কমে, কোড পরিষ্কার হয়।
✘ যেখানে var টাইপ লুকায়
var result = process(data); — কী ফেরত এলো, পাঠক জানে না। var total = 0; হয় int, অথচ দরকার ছিল long. var list = new ArrayList<>(); হয় ArrayList<Object>!
ভালো নিয়ম: var ব্যবহার করুন যখন ডান দিক দেখেই টাইপ এক নজরে বোঝা যায়, আর ভ্যারিয়েবলের নাম অর্থপূর্ণ। সংখ্যার ক্ষেত্রে, যেখানে int না long না double — এই সিদ্ধান্তটাই আসল, সেখানে টাইপ নিজে লিখুন। কোড একবার লেখা হয়, কিন্তু পড়া হয় বহুবার।
৮ · String, default value আর reference type-এর ঝলক
লেখা রাখার String এত বেশি ব্যবহৃত যে অনেকে একে primitive ভাবেন। তা নয় — String একটি class, আর String s = "ঢাকা";-এর s একটি reference type ভ্যারিয়েবল, যেমন প্রথম stepper-এ দেখেছেন। তাই দুটি String-এর লেখা মেলে কি না দেখতেও == নয়, equals. String কেন বদলানো যায় না (immutable), StringBuilder কখন লাগে — পুরো আলোচনা পর্ব ০৮-এ।
Field পায় default value, local পায় না
অংশ ২-এ দেখেছেন, local ভ্যারিয়েবলে মান না বসিয়ে পড়া যায় না। কিন্তু class-এর field — object-এর ভেতরের ভ্যারিয়েবল — আলাদা। Object তৈরি হলে JVM তার সব field-এর জায়গা শূন্য দিয়ে মুছে দেয়, তাই প্রতিটি field একটি default value নিয়ে শুরু করে:
| Field-এর টাইপ | Default value |
|---|---|
byte, short, int, long | 0 |
float, double | 0.0 |
char | '\u0000' (কোড 0 — ছাপলে কিছু দেখা যায় না) |
boolean | false |
যেকোনো reference type (String, Integer, array …) | null |
| local ভ্যারিয়েবল (যেকোনো টাইপ) | কিছুই না — মান না বসিয়ে পড়লে compile error |
এই পার্থক্য কেন? Field-এ কখন কে মান বসাবে — constructor, অন্য method, নাকি কখনোই না — compiler সবসময় নিশ্চিত হতে পারে না, তাই JVM নিরাপদ শূন্য দিয়ে শুরু করে। Local ভ্যারিয়েবলের পুরো জীবন একটি method-এর ভেতরে, তাই compiler সব পথ পরীক্ষা করে নিশ্চিত হতে পারে — আর পারে বলেই কড়াকড়ি করে, কারণ মান ভুলে যাওয়া প্রায় সবসময় একটি বাগ। একটি সতর্কতা: String field-এর default null, খালি লেখা "" নয়; name.length() ডাকলে NullPointerException. Field আর object-এর বিস্তারিত পর্ব ১৫-এ।
- Java-তে কয়টি primitive type, কী কী? — ৮টি:
byte,short,int,long,float,double,char,boolean;Stringএর মধ্যে নেই। intআরInteger-এর পার্থক্য? —intprimitive, মান সরাসরি রাখে,nullহয় না;Integerobject,nullহতে পারে, collection-এ লাগে, তুলনায়equals.Integer a = 128, b = 128; a == bকী দেয়? — সাধারণতfalse: −128 থেকে 127-এর বাইরেvalueOfনতুন object বানায়, আর==reference তুলনা করে।(byte) 200-এর মান কত? — −56: শুধু শেষ ৮ bit11001000থাকে, যা two's complement-এ 200 − 256.charকত bit, আর কেন C-র মতো ৮ নয়? — ১৬ bit, কারণ Java শুরু থেকেই Unicode (এখন UTF-16) ব্যবহার করে।
৯ · AI-এর যুগে — টাইপের ভুল ধরার চোখ
AI-এর লেখা Java কোড সাধারণত compile হয়, আর ছোট উদাহরণে ঠিক ফলও দেয়। টাইপের ভুল ঠিক সেই জাতের, যা ছোট পরীক্ষায় ধরা পড়ে না — বড় সংখ্যা, টাকার হিসাব বা null ডেটা এলে তবেই ফুটে ওঠে। এই পর্বের প্রতিটি ফাঁদ AI-এর কোডে নিয়মিত দেখা যায়:
| AI যা প্রায়ই লেখে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
double price, double balance | দশমিকের কাটাকাটিতে পয়সার গরমিল | BigDecimal (String থেকে) বা পয়সায় long |
if (userId == otherId) — দুটিই Integer | 127-এর পর ভুল ফল | userId.equals(otherId) |
int total = count * price; | বড় ডেটায় নীরব overflow | long, বা Math.multiplyExact |
সব জায়গায় var | টাইপ লুকিয়ে যায়, int/long ভুল ধরা কঠিন | টাইপ স্পষ্ট হলে তবেই var |
new Integer(x), new Double(x) | Java 9 থেকে deprecated, সতর্কবার্তা | Integer.valueOf(x) বা autoboxing |
AI-কে কোড লিখতে দেওয়ার সময়েই টাইপের শর্তগুলো বেঁধে দিন। নিচের prompt-টি কপি করে নিজের কাজ অনুযায়ী বদলে নিন:
AI-এর কোড হাতে পেলে — টাইপের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Smalltalk-এ সবকিছুই object. তাহলে Java কেন “সবকিছু object” না করে আলাদা primitive রেখে দিল?
১৯৯০-এর দশকের শুরুতে যখন James Gosling-এর দল Java নকশা করছিল, তখন object-oriented জগতের আদর্শ ছিল Smalltalk — যেখানে সংখ্যা 3-ও একটি object, আর 3 + 4 মানে 3 নামের object-কে “+ 4” বার্তা পাঠানো। ধারণাটি সুন্দর ও সামঞ্জস্যপূর্ণ। কিন্তু সেই যুগের হার্ডওয়্যারে এর দাম ছিল চড়া, আর Smalltalk-এর ধীরগতির খ্যাতিও ছিল।
Java-র লক্ষ্য ছিল C ও C++ প্রোগ্রামারদের টেনে আনা, আর সীমিত মেমরির যন্ত্রে চলা। একটি int মানে ৪ byte, সরাসরি CPU-র register-এ যোগ হয়। অথচ একটি Integer object মানে heap-এ আলাদা বরাদ্দ, object header (আজকের ৬৪-bit HotSpot-এ সাধারণত ১২ byte), তারপর ৪ byte মান, সব মিলিয়ে প্রায় ১৬ byte — আর তাকে পেতে একটি reference ধরে মেমরির অন্য জায়গায় যাওয়া। এক লাখ সংখ্যার int[] একটানা ৪ লাখ byte; সমান সংখ্যক Integer-এর তালিকা মানে এক লাখ reference আর এক লাখ ছড়ানো object. আধুনিক CPU একটানা মেমরি পড়তে অনেক দ্রুত (cache-এর কারণে), তাই পার্থক্য শুধু মেমরিতে নয়, গতিতেও কয়েক গুণ।
Java-র নকশাকারদের পরবর্তী আলোচনায় বারবার এসেছে যে primitive রাখা ছিল মূলত পারফরম্যান্সের জন্য একটি বাস্তববাদী আপস। দাম হলো ভাষার ভেতরে একটি “ফাটল”: দুই জগতের জন্য আলাদা নিয়ম, wrapper class, আর Java 5-এ (২০০৪) আসা autoboxing — যা লেখা সহজ করলেও Integer cache আর null unboxing-এর মতো নতুন ফাঁদ তৈরি করেছে। Generics-এ List<int> লেখা যায় না, এটিও এই ফাটলের ফল।
অন্য ভাষাগুলো ভিন্ন পথ নিয়েছে। C# শুরু থেকেই (২০০০) “value type” রেখেছে, যা ব্যবহারকারী নিজেও বানাতে পারেন। Kotlin উৎস-কোডে শুধু Int দেখায়, আর compiler ঠিক করে নেয় কোথায় JVM-এর primitive আর কোথায় object বসবে। Python সবকিছু object রেখেছে, দাম দিয়েছে গতিতে।
মূল উপলব্ধি: ভাষার নকশা মানে নিখুঁত ধারণা আর তখনকার হার্ডওয়্যারের বাস্তবতার মধ্যে আপস। Java-র primitive ছিল ১৯৯০-এর দশকের সঠিক সিদ্ধান্ত, যার জটিলতা তিন দশক পরেও ভাষা বহন করছে — আর সেটা মেরামতের চেষ্টাই পরের প্রশ্নের বিষয়।
প্র ০২Project Valhalla কী? “Codes like a class, works like an int” — Java কি primitive আর object-এর ফাটল জোড়া লাগাতে পারবে?
আগের প্রশ্নে দেখেছেন, Java-র টাইপ দুই ভাগে বিভক্ত — দ্রুত কিন্তু সীমিত primitive, আর নমনীয় কিন্তু ভারী object. ২০১৪ সালে OpenJDK-তে Project Valhalla নামে একটি দীর্ঘমেয়াদি প্রকল্প শুরু হয়, যার নেতৃত্বে Oracle-এর Java ভাষা-স্থপতি Brian Goetz ও JVM-বিশেষজ্ঞ John Rose. লক্ষ্য এক বাক্যে: এমন টাইপ, যা “লেখা যায় class-এর মতো, কিন্তু কাজ করে int-এর মতো”।
মূল সমস্যাটা হলো object-এর পরিচয় (identity). প্রতিটি object-এর নিজস্ব পরিচয় আছে — দুটি Integer-এ একই 128 থাকলেও == তাদের আলাদা বলে, কারণ JVM-কে তাদের আলাদা জিনিস হিসেবে মনে রাখতে হয়। এই পরিচয়ের জন্যই object-কে heap-এ আলাদা জায়গায় রাখতে হয় আর reference দিয়ে ধরতে হয়। কিন্তু একটি তারিখ, একটি টাকার অঙ্ক বা একটি ২-মাত্রিক বিন্দুর কি সত্যিই “পরিচয়” দরকার? দুটি বিন্দু (3, 4) আর (3, 4) — এরা কি আলাদা? সাধারণত না; শুধু মানটাই গুরুত্বপূর্ণ।
Valhalla-র প্রস্তাব হলো value class — এমন class, যার object-এর কোনো পরিচয় নেই, শুধু মান। পরিচয় না থাকলে JVM স্বাধীনভাবে সেগুলোকে array-র ভেতরে একটানা বসাতে পারে (flattening), stack-এ রাখতে পারে, header বাদ দিতে পারে — ঠিক int-এর মতো। আর == তখন মান তুলনা করে। এই কাজের মূল প্রস্তাবটি JEP 401 (“Value Classes and Objects”). দীর্ঘমেয়াদি স্বপ্ন হলো List<int>-এর মতো generics, আর Integer-এর মতো wrapper-কেও value class বানিয়ে Integer cache-এর মতো ফাঁদ অর্থহীন করে দেওয়া।
দশ বছরের বেশি সময় লাগছে কেন? কারণ Java-র সবচেয়ে বড় প্রতিশ্রুতি পুরোনো কোডের সাথে সামঞ্জস্য। কোটি কোটি লাইন কোড, যা Integer-এর পরিচয়, synchronized বা null-এর ওপর নির্ভর করে, সেগুলো ভাঙা চলবে না। দল একাধিকবার নকশা ফেলে দিয়ে নতুন করে শুরু করেছে। এই লেখার সময় পর্যন্ত value class কোনো Java version-এ স্থায়ী (final) বৈশিষ্ট্য হিসেবে আসেনি; কাজ চলছে মূলত পরীক্ষামূলক (early-access) build আর preview প্রস্তাবের পর্যায়ে। তাই কবে আসবে, এ বিষয়ে কারও নিশ্চিত দাবি — বিশেষ করে AI-এর — যাচাই করে নেবেন।
মূল উপলব্ধি: জনপ্রিয় ভাষার একটি মৌলিক নকশা-সিদ্ধান্ত বদলানো নতুন ভাষা বানানোর চেয়ে অনেক কঠিন, কারণ অতীতের সব কোডকে সাথে নিয়ে চলতে হয়। এটাই Java-র দীর্ঘ জীবনের রহস্য, আবার ধীর পরিবর্তনেরও কারণ।
প্র ০৩Java-র char কেন ১৬ bit? আর তবু কেন একটি ইমোজির জন্য দুটি char লাগে?
C-র char ৮ bit — ইংরেজি অক্ষরের জন্য যথেষ্ট, কিন্তু বাংলা, হিন্দি, চীনা বা আরবি লিপির জন্য একেবারেই নয়। ১৯৮০-এর দশকে প্রতিটি দেশ ও কোম্পানি নিজের নিজের encoding বানাত; বাংলার জন্যও ছিল নানা বেমানান ফন্ট-ভিত্তিক ব্যবস্থা। একটি ফাইল এক কম্পিউটারে লিখে আরেকটিতে খুললে অর্থহীন চিহ্ন দেখা যেত।
এর সমাধান হিসেবে ১৯৯১ সালে প্রকাশিত হয় Unicode 1.0 — পৃথিবীর সব লিপির জন্য একটিমাত্র কোড-তালিকা। আর তখনকার মূল নকশা-ধারণা ছিল: ১৬ bit, অর্থাৎ ৬৫,৫৩৬টি কোডই সব জীবন্ত লিপির জন্য যথেষ্ট। ঠিক এই সময়েই Java-র নকশা হচ্ছিল। আন্তর্জাতিক ভাষা হতে চাওয়া Java তাই সিদ্ধান্ত নিল: char হবে ১৬ bit, প্রতিটি char একটি Unicode অক্ষর। একই যুক্তিতে Windows NT আর পরে JavaScript-ও ১৬-bit অক্ষর বেছে নেয়।
কিন্তু শীঘ্রই বোঝা গেল ৬৫,৫৩৬ যথেষ্ট নয় — চীনা-জাপানি-কোরীয় লিপির হাজার হাজার বাড়তি অক্ষর, ঐতিহাসিক লিপি, গাণিতিক চিহ্ন, আর পরে ইমোজি। ১৯৯৬ সালে Unicode 2.0 কোডের পরিসর বাড়িয়ে প্রায় ১১ লাখে নিয়ে যায়। ১৬ bit-এর বাইরের অক্ষর রাখার কৌশল হলো surrogate pair: একটি বিশেষ সংরক্ষিত পরিসরের দুটি ১৬-bit ঘর মিলে একটি অক্ষর। এই পদ্ধতির নাম UTF-16. Java-র char রাতারাতি “একটি অক্ষর” থেকে হয়ে গেল “UTF-16-এর একটি ঘর”। Java 5 (২০০৪) এই বাড়তি অক্ষরগুলোর পূর্ণ সমর্থন যোগ করে — codePointAt, codePoints()-এর মতো method দিয়ে।
ফলাফল আজও দেখা যায়: "😀".length() ফেরত দেয় 2, কারণ length() অক্ষর নয়, char গোনে। বাংলা অক্ষরগুলো (U+0980 থেকে U+09FF) প্রথম ৬৫,৫৩৬-এর মধ্যে, তাই প্রতিটি এক char-এ ধরে; কিন্তু মানুষের চোখে এক অক্ষর “ক্ষ” বা “কি” আসলে একাধিক কোড। মেমরির দিকেও দাম ছিল — ইংরেজি লেখার জন্যও প্রতি অক্ষরে ২ byte. তাই Java 9-এ (JEP 254, compact strings) String ভেতরে ভেতরে শুধু Latin-1 অক্ষর থাকলে প্রতি অক্ষরে ১ byte রাখে — বাইরে থেকে আচরণ একই।
মূল উপলব্ধি: “এটা যথেষ্ট হবে” — নকশার সবচেয়ে বিপজ্জনক অনুমান। ১৯৯১-এর একটি যুক্তিসঙ্গত সিদ্ধান্ত তিন দশক পরেও প্রতিটি Java String-এর আচরণ ঠিক করছে। লেখা গোনার আগে ভাবুন: আপনি char গুনছেন, Unicode কোড, নাকি মানুষের দেখা অক্ষর?
প্র ০৪১৯৯৬ সালে Ariane 5 রকেট কেন উড্ডয়নের ৪০ সেকেন্ডের মধ্যে ধ্বংস হলো? একটি টাইপ রূপান্তর কতটা দামি হতে পারে?
৪ জুন ১৯৯৬, ফরাসি গায়ানা থেকে ইউরোপিয়ান স্পেস এজেন্সির নতুন রকেট Ariane 5-এর প্রথম উড্ডয়ন (Flight 501). প্রায় ৩৭ সেকেন্ড পর রকেটটি হঠাৎ পথ থেকে সরে যায়, প্রবল বাতাসের চাপে ভাঙতে শুরু করে, আর স্বয়ংক্রিয় ব্যবস্থায় নিজেই ধ্বংস হয়। সাথে ছিল চারটি বৈজ্ঞানিক উপগ্রহ। ক্ষতি কয়েকশো মিলিয়ন ডলার, আর এক দশকের কাজ।
তদন্ত বোর্ড (গণিতবিদ Jacques-Louis Lions-এর নেতৃত্বে) কারণ খুঁজে পায় রকেটের দিক-নির্ণয় ব্যবস্থার (Inertial Reference System) সফটওয়্যারে। সেখানে একটি মান — রকেটের অনুভূমিক গতির সাথে সম্পর্কিত — রাখা ছিল ৬৪-bit floating-point সংখ্যায়, আর কোডের এক জায়গায় সেটিকে ১৬-bit চিহ্নযুক্ত পূর্ণসংখ্যায় রূপান্তর করা হচ্ছিল। ১৬ bit-এ সর্বোচ্চ 32,767. Ariane 5 তার পূর্বসূরির চেয়ে অনেক দ্রুত গতি তোলে, ফলে মানটি সেই সীমা ছাড়িয়ে যায়। এটা ঠিক এই পর্বের narrowing conversion-এর সমস্যা।
সফটওয়্যারটি লেখা ছিল Ada ভাষায়, যা Java-র মতো চুপচাপ ঘুরে না গিয়ে একটি exception ছুড়েছিল। কিন্তু দক্ষতার জন্য এই নির্দিষ্ট রূপান্তরের exception সামলানোর কোড রাখা হয়নি। ফলে দিক-নির্ণয় কম্পিউটারটি বন্ধ হয়ে যায়। ব্যাকআপ কম্পিউটারও ছিল — কিন্তু সেটিতে একই সফটওয়্যার, তাই সেটি মুহূর্ত আগে একইভাবে বন্ধ হয়েছিল। মূল কম্পিউটার তখন ত্রুটি-তথ্যকে উড়ানের তথ্য ভেবে রকেটকে চরম দিকে ঘুরিয়ে দেয়।
সবচেয়ে শিক্ষণীয় অংশ: কোডটি এসেছিল Ariane 4 থেকে, যেখানে বছরের পর বছর নিখুঁত কাজ করেছে — কারণ Ariane 4 এত গতি তুলত না যে মানটি সীমা ছাড়ায়। আর যে হিসাবটির জন্য রূপান্তর, সেটি উড্ডয়নের পর Ariane 5-এ কোনো কাজেই লাগত না! সীমার অনুমানটি কোথাও স্পষ্ট লেখা ও নতুন রকেটের জন্য যাচাই করা হয়নি।
Java-তে একই ভুল লিখলে কী হতো? (short) speed exception দিত না — চুপচাপ ভুল সংখ্যা দিত, যা হয়তো আরও খারাপ। সঠিক পথ হলো সীমা যাচাই করা, বা Math.toIntExact-এর মতো method দিয়ে সীমা ছাড়ালে স্পষ্টভাবে থামা, আর থামলে কী হবে তার পরিকল্পনা রাখা।
মূল উপলব্ধি: প্রতিটি narrowing cast একটি লুকানো অনুমান — “এই মান কখনো এত বড় হবে না”। কোড পুনর্ব্যবহারের সময় সেই অনুমানগুলোও সাথে চলে আসে। একই সফটওয়্যারের দুটি কপি একটি ব্যাকআপ নয়, একই বাগের দুটি কপি।
প্র ০৫var আনতে Java এত বছর দেরি করল কেন? JEP 286-এর বিতর্কে কী কী প্রশ্ন উঠেছিল?
C# ২০০৭ সালে (C# 3.0) var এনেছিল, Scala আর Kotlin-এ শুরু থেকেই type inference ছিল। Java প্রোগ্রামাররা বহু বছর ধরে লম্বা লাইনের অভিযোগ করতেন — Map<String, List<Integer>> m = new HashMap<String, List<Integer>>();-এ একই টাইপ দুবার। Java 7 diamond (<>) এনে ডান দিকটা ছোট করেছিল, কিন্তু বাঁ দিক রয়ে গিয়েছিল।
২০১৬ সালে Brian Goetz প্রস্তাব করেন JEP 286: Local-Variable Type Inference. প্রস্তাবটির একটি অস্বাভাবিক দিক ছিল: OpenJDK দল Java ডেভেলপারদের মধ্যে একটি অনলাইন জরিপ চালায় — ধারণাটি কেমন লাগে, আর কোন শব্দ পছন্দ; বিকল্পের মধ্যে ছিল যেমন শুধু var, Scala-র মতো পরিবর্তনযোগ্যের জন্য var আর অপরিবর্তনীয়ের জন্য val, কিংবা let. প্রকাশিত ফল অনুযায়ী উত্তরদাতাদের বড় অংশ ধারণাটি সমর্থন করেন। শেষে বেছে নেওয়া হয় শুধু var; অপরিবর্তনীয়তার জন্য আগে থেকেই আছে final var, আর var/val দেখতে এত কাছাকাছি যে পড়তে ভুল হওয়ার আশঙ্কা ছিল।
বিরোধীদের যুক্তিও গুরুতর ছিল। Java-র শক্তি হলো কোড পড়ে বোঝা যায় কী কী আছে; var টাইপ লুকিয়ে সেই পঠনযোগ্যতা নষ্ট করবে, বিশেষ করে IDE ছাড়া কোড রিভিউয়ে (যেমন GitHub-এ)। সমঝোতা হলো কঠোর সীমা: শুধু local ভ্যারিয়েবলে, যেখানে প্রভাব একটি method-এর ভেতরেই সীমিত। Field, parameter বা return type-এ নয় — কারণ সেগুলো একটি class-এর বাইরের চুক্তি, আর চুক্তির টাইপ স্পষ্ট লেখা থাকা উচিত। var-কে keyword না বানিয়ে “reserved type name” রাখা হয়, যাতে পুরোনো কোডে var নামের ভ্যারিয়েবল ভেঙে না যায় — Java-র পেছনের সামঞ্জস্যের সেই চিরচেনা প্রতিশ্রুতি।
var আসে Java 10-এ (মার্চ ২০১৮), আর Java 11-এ lambda-র parameter-এও ব্যবহারের সুযোগ যোগ হয় (JEP 323). সাথে OpenJDK প্রকাশ করে ব্যবহারের একটি style guide (Stuart Marks-এর লেখা), যার মূল কথা: ভ্যারিয়েবলের নাম অর্থপূর্ণ রাখুন, scope ছোট রাখুন, আর যেখানে টাইপ পাঠকের জন্য গুরুত্বপূর্ণ তথ্য, সেখানে লিখুন।
মূল উপলব্ধি: ভাষার একটি ছোট বৈশিষ্ট্যের পেছনেও থাকে লেখার সুবিধা আর পড়ার স্পষ্টতার মধ্যে টানাপোড়েন। var-এর সীমাগুলো খেয়ালখুশি নয় — প্রতিটি সীমা একটি সচেতন সিদ্ধান্ত যে কোথায় টাইপ লুকানো চলে আর কোথায় চলে না।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে-কলমে দেখা আর ভাবাই আসল কাজ।
-
টাইপ বাছাই: নিচের প্রতিটির জন্য সবচেয়ে মানানসই টাইপ বলুন, এক লাইনে কারণসহ — (ক) একটি ক্লাসের শিক্ষার্থী সংখ্যা, (খ) একটি ব্যাংকের মোট আমানত (টাকায়, পয়সাসহ), (গ) কোনো পণ্য স্টকে আছে কি না, (ঘ) একটি ভিডিওর মোট দেখা (view) সংখ্যা, (ঙ) রক্তের গ্রুপের অক্ষর (A, B, O).
(ক)
int— কয়েকশোর বেশি হবে না, আরint-ই পূর্ণসংখ্যার স্বাভাবিক পছন্দ;byte/shortদিয়ে এখানে কোনো লাভ নেই। (খ)BigDecimal— টাকা, দশমিক আর বিশাল অঙ্ক, সাথে সুদের হিসাবে রাউন্ডিং; সরল হিসাবে পয়সায়long-ও চলে, কিন্তু কখনোdoubleনয়। (গ)boolean. (ঘ)long— জনপ্রিয় ভিডিওর দেখা ২১৪ কোটি ছাড়াতে পারে, আর বাস্তবে একটি বিখ্যাত ভিডিও-সাইটকে ঠিক এই কারণে ৩২-bit কাউন্টার থেকে ৬৪-bit-এ যেতে হয়েছিল। (ঙ)charযদি শুধু একটি অক্ষর; তবে “AB” বা “O+”-এর মতো মান রাখতে হলেString, আর আরও ভালো হয় পর্ব ২৪-এরenum. -
আউটপুট অনুমান: না চালিয়ে বলুন কী ছাপবে, তারপর উপরের রানারে চালিয়ে মিলিয়ে নিন —
(int) 9.99,(byte) 130,7 / 2,7 / 2.0,Integer.MAX_VALUE + 1,(char) 66.9(দশমিক কাটা, রাউন্ড নয়);-126(130-এর শেষ ৮ bit10000010, প্রথম bit 1, তাই 130 − 256);3(দুই পাশইint);3.5(এক পাশdouble);-2147483648(overflow, ঘুরে সবচেয়ে ছোট মানে);B(কোড 66 মানে Unicode-এ বড় হাতের B;charআসলে একটি সংখ্যা)। -
Compiler-এর error পড়ুন: নিচের প্রতিটি লাইন আলাদাভাবে compile করলে কোনটি পাস করবে, কোনটি করবে না, আর কেন?
float f = 2.5;·long n = 3_000_000_000;·byte b = 100;·var v;·int x = 'A';float f = 2.5;✘ —2.5একটিdoubleliteral,float-এ নামাতে cast লাগে:possible lossy conversion from double to float; সমাধান2.5f.long n = 3_000_000_000;✘ —integer number too large, কারণ literal-টিint; সমাধান শেষেL.byte b = 100;✔ — constant 100byte-এর সীমায় (−128 থেকে 127), তাই compiler মানিয়ে নেয়।var v;✘ — মান ছাড়া অনুমানের কিছু নেই:cannot use 'var' on variable without initializer.int x = 'A';✔ —charথেকেintwidening,xহয় 65. -
AI অডিট: কোনো AI-কে শর্ত ছাড়া বলুন: “Java-তে একটি shopping cart class লিখে দাও যা পণ্যের দাম যোগ করে মোট টাকা আর ১৫% ভ্যাট হিসাব করে।” তারপর অংশ ৯-এর টেবিল আর চেকলিস্ট ধরে উত্তরটি যাচাই করুন।
প্রায়ই দেখা যায়:
- দাম আর মোট
double-এ — কয়েকটি দাম যোগ করে ভ্যাট গুণ করলে...0000001ধরনের লেজ দেখা দেয়। BigDecimalথাকলেওnew BigDecimal(price)-এ double থেকে বানানো, বা রাউন্ডিংয়ের নিয়ম (setScale) দেওয়া নেই।- পণ্যের সংখ্যা বা id
Integer, আর তুলনায়==.
এরপর একই কাজ অংশ ৯-এর prompt-এর মতো শর্ত দিয়ে চান, আর দুটি উত্তর পাশাপাশি রেখে দেখুন কোন কোন জায়গা বদলাল। চেকলিস্টের প্রতিটি ঘরে টিক দিতে পারলে তবেই কোডটি বিশ্বাস করুন।
- দাম আর মোট
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJava-র সংখ্যার টাইপগুলো widening-এর ক্রমে সাজান — যেটিতে cast ছাড়াই পরেরটিতে যাওয়া যায় (▲▼ চেপে সরান)
byte— ৮ bitshort— ১৬ bitint— ৩২ bitlong— ৬৪ bitfloat— ৩২ bit দশমিকdouble— ৬৪ bit দশমিক
Java-তে byte টাইপের ভ্যারিয়েবলে কোন পরিসরের মান রাখা যায়?
- 0 থেকে 255
- −128 থেকে 127
- −32,768 থেকে 32,767
- JVM-এর ওপর নির্ভর করে
Integer x = 128, y = 128; — সাধারণ JVM সেটিংসে x == y আর x.equals(y) কী দেয়?
- দুটিই false
- দুটিই true
- x == y হলো false, x.equals(y) হলো true
- x == y হলো true, x.equals(y) হলো false
Java-তে (int) 3.99-এর মান কত?
- 3
- 4
- Compile error
- 3.99
একটি method-এর ভেতরে int count; ঘোষণা করে মান না বসিয়েই System.out.println(count); লিখলে কী হয়?
- 0 ছাপে
- মেমরিতে আগে থেকে থাকা কোনো এলোমেলো মান ছাপে
- চলার সময় NullPointerException
- Compile error — variable might not have been initialized
একটি ই-কমার্স অ্যাপে পণ্যের দাম (টাকা ও পয়সা) আর ভ্যাটসহ মোট হিসাব রাখতে কোনটি সবচেয়ে উপযুক্ত?
- double
- String থেকে বানানো BigDecimal
- float
- টাকার অঙ্ক int-এ
Java-র var সম্পর্কে কোন কথাটি সঠিক?
- var ভ্যারিয়েবলে পরে যেকোনো টাইপের মান রাখা যায়
- var দিয়ে class-এর field ঘোষণা করা যায়
- টাইপ compile-এর সময় ঠিক হয়, তাই var x; বা var x = null; compile হয় না
- var প্রোগ্রাম চলার সময় টাইপ খোঁজে, তাই একটু ধীর
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Java statically typed: প্রতিটি ভ্যারিয়েবলের টাইপ compile-এর সময় ঠিক; local ভ্যারিয়েবল মান না বসিয়ে পড়া যায় না।
- ৮টি primitive ঘরে মান রাখে, আকার নির্দিষ্ট; reference type ঘরে রাখে heap-এর object-এর খোঁজ। Java সবসময় ঘরের ভেতরের জিনিস কপি করে।
- পূর্ণসংখ্যা ভরে গেলে নীরবে ঘুরে যায় (
Math.addExactদিয়ে ধরুন); দশমিক নির্ভুল নয় — টাকায়BigDecimal("…")বা পয়সায়long. - Widening নিজে হয়; narrowing-এ cast লাগে আর তথ্য হারাতে পারে —
(int) 3.99= 3,(byte) 200= −56;7 / 2= 3. - Wrapper object তুলনায়
equals,nullunbox করলে NullPointerException;varটাইপ লেখা বাঁচায়, টাইপ মুছে দেয় না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৭ · অপারেটর — হিসাব, তুলনা আর যুক্তিপরবর্তী পর্বটাইপগুলো দিয়ে হিসাব, তুলনা আর সিদ্ধান্ত।
- Java প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python-এর ভ্যারিয়েবল ও টাইপতুলনা করুনDynamic typing আর সীমাহীন int — একই প্রশ্নের ভিন্ন উত্তর।
- C-এর ভ্যারিয়েবল ও টাইপপাশের ট্র্যাকপ্ল্যাটফর্মভেদে বদলানো আকার, signed আর unsigned.