এই পর্বে যা শিখবেন
- হিসাবের পাঁচটি অপারেটর, integer division আর ঋণাত্মক সংখ্যার
%— বিল ভাগ করার বাস্তব উদাহরণে - Numeric promotion, compound assignment-এর লুকানো cast, আর
++/---এর আগে-পরে (i = i++ফাঁদসহ) - তুলনা ও যুক্তির অপারেটর, short-circuit, ternary — আর কেন
==দিয়ে String মেলানো বিপজ্জনক - Bitwise অপারেটর আর Java-র নিজস্ব
>>>— permission flag-এর উদাহরণে - Precedence, বাঁ থেকে ডানে মূল্যায়নের নিয়ম,
Mathclass — আর AI-এর লেখা কোডে অপারেটরের ভুল ধরা
১ · অপারেটর কী — মান থেকে নতুন মান
পর্ব ০৬-এ আপনি ভ্যারিয়েবলে মান রাখতে শিখেছেন। কিন্তু শুধু মান রেখে দিলে প্রোগ্রাম কিছুই করে না। দোকানের হিসাব, পরীক্ষার গড়, লগইনের অনুমতি — সব জায়গায় পুরোনো মান থেকে নতুন মান বানাতে হয়। এই কাজ করে operator: এমন একটি চিহ্ন, যা এক বা একাধিক মান নিয়ে একটি নতুন মান তৈরি করে। যে মানগুলোর ওপর অপারেটর কাজ করে, তাদের বলে operand. আর operand ও অপারেটর মিলে যা দাঁড়ায়, যেমন price * qty, তাকে বলে expression — প্রতিটি expression শেষ পর্যন্ত একটি মান দেয়।
Java-র অপারেটরগুলোকে কাজ অনুযায়ী তিনটি বড় দলে ভাবা যায়:
🧮 হিসাব করে
+ - * / % — সংখ্যা থেকে সংখ্যা। বিলের মোট, গড়, ভাগশেষ।
❓ প্রশ্ন করে
== != < > <= >= — “বয়স ১৮-এর বেশি?” উত্তর শুধু হ্যাঁ বা না।
🔗 শর্ত জোড়ে
&& || ! — “member এবং ২০০০ টাকার বেশি কেনাকাটা”।
⚙️ bit নাড়ে
& | ^ ~ << >> >>> — সংখ্যার ভেতরের ০ আর ১ নিয়ে সরাসরি কাজ।
এতটুকু সহজ। কঠিন অংশ অন্য জায়গায়: একই চিহ্নের ফল নির্ভর করে operand-এর টাইপ আর হিসাবের ক্রমের ওপর। 7 / 2 আর 7.0 / 2 দেখতে প্রায় এক, ফল আলাদা। a == b দুটি int-এর বেলায় এক কাজ করে, দুটি String-এর বেলায় আরেক কাজ। এই নিয়মগুলো না জানলে প্রোগ্রাম কোনো error দেখায় না — শুধু চুপচাপ ভুল উত্তর দেয়। এ ধরনের বাগ সবচেয়ে বিপজ্জনক, কারণ কেউ টেরই পায় না।
প্রতিটি expression-এর একটি টাইপ আছে, আর সেই টাইপ compile-এর সময়ই ঠিক হয়ে যায়। 7 / 2-এর টাইপ int, তাই ফলও int — চলার সময় Java “বুদ্ধি করে” দশমিক বানিয়ে দেয় না। অপারেটর শেখা মানে আসলে এই প্রশ্নের উত্তর জানা: “এই expression-এর টাইপ কী হবে, আর কোন অংশ আগে হিসাব হবে?”
২ · হিসাবের অপারেটর — ভাগে সাবধান
যোগ (+), বিয়োগ (-) আর গুণ (*) স্কুলের অঙ্কের মতোই কাজ করে। চমক লুকিয়ে আছে বাকি দুটিতে — ভাগ (/) আর ভাগশেষ (%).
Integer division — ভগ্নাংশ কেটে ফেলা
দুটি operand-ই পূর্ণসংখ্যা (int বা long) হলে Java integer division করে: ভাগফলের দশমিক অংশ সোজা বাদ দেয়, গোল (round) করে না। 7 / 2 = 3, 9 / 10 = 0, -7 / 2 = -3. যেকোনো একটি operand দশমিক (double) হলেই পুরো হিসাব দশমিকে হয়: 7 / 2.0 = 3.5.
এটি সবসময় বাগ নয় — অনেক সময় ঠিক এটাই দরকার। ধরুন চার বন্ধু মিলে চায়ের দোকানে ১২৫০ টাকা বিল করলেন, আর ঠিক করলেন প্রত্যেকে পূর্ণ টাকায় দেবেন, যা খুচরা বাকি থাকবে তা যে বিল দিচ্ছে সে দেবে:
1250 % 4 = 2 (খুচরা বাকি)
% অপারেটরের নাম remainder বা modulo — ভাগশেষ দেয়। একটি নিয়ম সবসময় সত্য: (a / b) * b + a % b == a. অর্থাৎ ৩১২ × ৪ + ২ = ১২৫০. %-এর পরিচিত ব্যবহার: জোড়-বিজোড় যাচাই (n % 2 == 0), শেষ অঙ্ক বের করা (n % 10), সেকেন্ড থেকে মিনিট-সেকেন্ড (s / 60 আর s % 60), আর ঘুরে ঘুরে আসা হিসাব — যেমন সপ্তাহের দিন ((day + 3) % 7).
গড়ের সবচেয়ে পরিচিত বাগ: তিনটি পরীক্ষায় ৭০, ৮৫, ৯০ নম্বর। double avg = (a + b + c) / 3; লিখলে avg হয় 81.0, আসল গড় ৮১.৬৬… নয়। কারণ ডান দিকের পুরো হিসাব int-এ হয়ে 81 হয়েছে, তারপর সেটি double ভ্যারিয়েবলে রাখার সময় 81.0 হয়েছে — হারানো অংশ আর ফেরে না। ঠিক উপায়: / 3.0, বা (double) (a + b + c) / 3. ভ্যারিয়েবলের টাইপ নয়, expression-এর টাইপই ঠিক করে ভাগ কেমন হবে।
ঋণাত্মক সংখ্যার % — Java শূন্যের দিকে কাটে
Java-র integer division দশমিক অংশ সবসময় শূন্যের দিকে কেটে ফেলে (truncation toward zero). -7 / 2-এর আসল মান −৩.৫; শূন্যের দিকে কাটলে −৩. তাহলে উপরের নিয়ম অনুযায়ী -7 % 2 = −৭ − (−৩ × ২) = −১. অর্থাৎ Java-তে %-এর ফলের চিহ্ন হয় বাঁ দিকের operand-এর মতো: -7 % 2 = -1, কিন্তু 7 % -2 = 1.
এর একটি বাস্তব ফাঁদ আছে: n % 2 == 1 দিয়ে বিজোড় যাচাই করলে ঋণাত্মক বিজোড় সংখ্যায় ভুল হয়, কারণ -7 % 2 হলো -1, 1 নয়। নিরাপদ উপায় n % 2 != 0.
যদি গণিতের মতো “সবসময় অঋণাত্মক ভাগশেষ” চান — যেমন ঘড়ির হিসাব বা array-তে ঘুরে ঘুরে index — তাহলে Java 8 থেকে আছে Math.floorDiv(a, b) আর Math.floorMod(a, b). এরা নিচের দিকে (floor) কাটে: Math.floorDiv(-7, 2) = -4, Math.floorMod(-7, 2) = 1.
| Expression | Java | Python-এ একই হিসাব |
|---|---|---|
7 / 2 | 3 | 7 // 2 → 3 |
-7 / 2 | -3 (শূন্যের দিকে) | -7 // 2 → -4 (নিচের দিকে) |
-7 % 2 | -1 | -7 % 2 → 1 |
Math.floorMod(-7, 2) | 1 | Python-এর %-এর মতো |
5.5 % 2 | 1.5 (দশমিকেও চলে) | 1.5 |
C (C99 থেকে) আর C++ (C++11 থেকে) ঠিক Java-র মতোই শূন্যের দিকে কাটে, তাই -7 % 2 সেখানেও -1 (C অপারেটর পর্ব)। Python-এ / সবসময় দশমিক দেয়, পূর্ণসংখ্যার ভাগের জন্য আলাদা //, আর তার % নিচের দিকে কাটে — Java-র Math.floorMod-এর মতো (Python অপারেটর পর্ব)। Python থেকে এলে সবচেয়ে বড় পরিবর্তন: Java-তে দুটি int-এর / ভগ্নাংশ ফেলে দেয়। আর Java-তে ** নেই — ঘাত বের করতে Math.pow.
শূন্য দিয়ে ভাগেও টাইপ ভেদে আচরণ আলাদা। 5 / 0 (দুটিই int) চলার সময় ArithmeticException: / by zero ছোড়ে। কিন্তু 5.0 / 0 কোনো exception দেয় না, ফল দেয় Infinity; আর 0.0 / 0 দেয় NaN (Not a Number). দশমিক সংখ্যার এই নিয়ম আসে IEEE 754 মান থেকে, যা প্রায় সব আধুনিক CPU মেনে চলে।
৩ · টাইপের নিয়ম — promotion, লুকানো cast আর overflow
পর্ব ০৬-এ দেখেছেন, Java-তে পূর্ণসংখ্যার চারটি টাইপ: byte (৮ bit), short (১৬), int (৩২), long (৬৪)। এখন একটি অবাক করা নিয়ম: Java byte, short বা char নিয়ে সরাসরি হিসাব করে না। হিসাবের আগে এদের সবাইকে int-এ তুলে নেয়। একে বলে numeric promotion. নিয়মটি এক লাইনে:
নইলে long → long; নইলে (byte, short, char, int সব) → int
ফলে দুটি byte যোগ করলেও ফল int. আর int-কে চুপচাপ byte-এ ভরলে মান হারাতে পারে, তাই Java অনুমতি দেয় না:
byte a = 10, b = 20;
byte c = a + b; // a + b-এর টাইপ int, byte নয়Main.java:4: error: incompatible types: possible lossy conversion from int to byte
byte c = a + b;
^
1 error
বার্তাটির মানে: “int থেকে byte-এ রূপান্তরে মান হারাতে পারে।” আপনি নিশ্চিত হলে নিজে cast করুন: byte c = (byte) (a + b); — তখন দায়িত্ব আপনার। কারণটাও যৌক্তিক: JVM-এর bytecode-এ byte যোগের আলাদা কোনো নির্দেশই নেই; পর্ব ০৫-এ দেখা iadd সব ছোট টাইপের জন্য কাজ করে।
char-ও এই নিয়মে পড়ে, যা অনেককে অবাক করে: 'A' + 1-এর ফল 66 (একটি int), 'B' নয়। কারণ 'A'-এর ভেতরের সংখ্যা ৬৫.
Compound assignment — যেখানে cast লুকিয়ে থাকে
x = x + 5-এর সংক্ষিপ্ত রূপ x += 5. একই রকম -=, *=, /=, %= আছে। এদের বলে compound assignment. কিন্তু এরা শুধু সংক্ষেপ নয় — এদের ভেতরে একটি cast লুকানো। Java-র নিয়ম অনুযায়ী b += 1 আসলে b = (byte) (b + 1). তাই:
byte b = 10;
b = b + 1; // compile error: int থেকে byte
b += 1; // ঠিক আছে — ভেতরে (byte) cast আছেসুবিধাজনক, কিন্তু ঝুঁকিও এখানে। byte b = 127; b += 1; compile হয়, চলে, আর b হয়ে যায় -128 — কোনো সতর্কবার্তা ছাড়াই। একইভাবে int total = 0; total += 2.7; compile হয়, আর total হয় 2; দশমিক অংশ নিঃশব্দে উধাও।
Overflow — সীমা পেরোলে উল্টো দিকে
পর্ব ০৬-এ দেখেছেন, int-এর সর্বোচ্চ মান ২১৪৭৪৮৩৬৪৭. তার সাথে ১ যোগ করলে Java কোনো error দেয় না; ফল ঘুরে চলে যায় সর্বনিম্ন মানে, -2147483648. একে বলে integer overflow. গুণের বেলায় এটি সহজেই ঘটে — যেমন মিলিসেকেন্ডের হিসাবে 365 * 24 * 60 * 60 * 1000 int-এ ধরে না। সমাধান দুটি: বড় সংখ্যার জন্য long (প্রথম operand-কে 365L লিখলে পুরো হিসাব long-এ হয়), আর যেখানে ভুল ফল মেনে নেওয়া যায় না — যেমন টাকার হিসাব — সেখানে Math.addExact, Math.multiplyExact. এরা overflow হলে চুপ থাকে না, ArithmeticException: integer overflow ছোড়ে।
৪ · ++ আর -- — আগে বাড়বে, না পরে?
i++ মানে i-কে ১ বাড়াও, i-- মানে ১ কমাও। এদের বলে increment/decrement operator. একা একটি লাইনে লিখলে i++; আর ++i;-এর মধ্যে কোনো পার্থক্য নেই। পার্থক্য দেখা দেয় যখন এরা একটি বড় expression-এর অংশ:
- Postfix
i++: expression-এর মান হয় পুরোনো মান; তারপরiবাড়ে।int i = 5; int x = i++;→x= ৫,i= ৬. - Prefix
++i: আগেiবাড়ে; expression-এর মান হয় নতুন মান।int i = 5; int x = ++i;→x= ৬,i= ৬.
লক্ষ করুন, i++ দুটি কাজ করে: একটি মান দেয়, আবার একটি ভ্যারিয়েবলও বদলায়। মান দেওয়ার বাইরে এই বাড়তি বদলকে বলে side effect. একটি expression-এ একই ভ্যারিয়েবলের ওপর একাধিক side effect থাকলে মাথা গুলিয়ে যায়। সবচেয়ে বিখ্যাত উদাহরণ:
1int i = 5;2i = i++;3System.out.println(i);
| i | |
| সাময়িক মান (i++-এর ফল) |
- শুরু।
i-তে ৫ রাখা হলো। - ডান দিক আগে:
i++-এর মান নেওয়া।=-এর ডান দিকের expression হিসাব হয়। Postfix-এর নিয়মেi++-এর মান হলো পুরোনো মান, ৫ — JVM সেটি একটি সাময়িক জায়গায় (পর্ব ০৫-এর operand stack-এ) রেখে দেয়। - Side effect:
iবাড়ে। এবারi++-এর বাড়তি কাজ —iহয়ে যায় ৬. এখানে থামলে সব ঠিক ছিল। - Assignment: পুরোনো মান ফেরত। সবশেষে
=তার কাজ করে: ডান দিকের মান — সেই রেখে দেওয়া ৫ —i-তে লেখে। ৬ মুছে আবার ৫ হয়ে গেল। বাড়ানোটা কার্যত বাতিল। - ফল:
5. Java-তে এটি সবসময়, সব JVM-এ ৫ — কারণ Java-র মূল্যায়নের ক্রম নির্দিষ্ট (অংশ ৮)। C-তে একই কোড undefined behaviour, অর্থাৎ যেকোনো কিছু হতে পারে। শিক্ষা:i = i++কখনো লিখবেন না। বাড়াতে চাইলে শুধুi++;
নিয়মটি সহজ রাখুন: ++ আর -- হয় নিজের লাইনে একা লিখুন (count++;), নয়তো for লুপের হালনাগাদ অংশে (পর্ব ১১)। a[i++] = i; বা x = i++ + ++i;-এর মতো কোড Java-তে নির্দিষ্ট ফল দিলেও পাঠকের মাথা ঘুরিয়ে দেয় — আর পরীক্ষার প্রশ্ন ছাড়া বাস্তবে এর কোনো দরকার নেই। ++ char-এও চলে: char ch = 'A'; ch++; → 'B' (এখানেও compound assignment-এর মতো লুকানো cast আছে)।
অনেক বইয়ে লেখা থাকে, ++ আর -- এসেছে PDP-11 কম্পিউটারের “autoincrement” নির্দেশ থেকে। Dennis Ritchie তাঁর “The Development of the C Language” প্রবন্ধে এই ধারণা নাকচ করেছেন: অপারেটর দুটি Ken Thompson তৈরি করেন B ভাষার জন্য, আর B-র প্রথম রূপ PDP-11 আসার আগের। Java অপারেটরগুলো C থেকে প্রায় হুবহু নিয়েছে — যাতে ১৯৯০-এর দশকের লাখো C/C++ প্রোগ্রামার নতুন ভাষায় অচেনা কিছু না দেখেন। কিন্তু Java একটি বড় বদল এনেছে: C-তে যেখানে হিসাবের ক্রম অনির্দিষ্ট, Java সেখানে নিয়ম বেঁধে দিয়েছে।
এ পর্যন্ত যা শিখলেন, সব একটি প্রোগ্রামে দেখুন। চালানোর আগে প্রতিটি লাইনের আউটপুট আন্দাজ করুন:
class Main {
public static void main(String[] args) {
int bill = 1250, friends = 4;
int each = bill / friends; // integer division
int left = bill % friends; // ভাগশেষ
System.out.println("প্রত্যেকে " + each + " টাকা, বাকি " + left + " টাকা");
System.out.println("গড় (ভুল): " + (70 + 85 + 90) / 3);
System.out.println("গড় (ঠিক): " + (70 + 85 + 90) / 3.0);
System.out.println("-7 / 2 = " + (-7 / 2) + ", -7 % 2 = " + (-7 % 2));
System.out.println("floorDiv = " + Math.floorDiv(-7, 2)
+ ", floorMod = " + Math.floorMod(-7, 2));
byte b = 127;
b += 1; // লুকানো cast: (byte) (b + 1)
System.out.println("byte 127 + 1 = " + b);
int i = 5;
i = i++;
System.out.println("i = i++ এর পরে i = " + i);
}
}আউটপুট হবে: প্রত্যেকে 312 টাকা, বাকি 2 টাকা; ভুল গড় 81 আর ঠিক গড় 81.66666666666667; -7 / 2 = -3, -7 % 2 = -1; floorDiv = -4, floorMod = 1; byte 127 + 1 = -128; আর শেষে i = i++ এর পরে i = 5. খেয়াল করুন, ভাগের expression-গুলো বন্ধনীতে রাখা হয়েছে। বন্ধনী ছাড়া "-7 / 2 = " + -7 / 2 ঠিকই কাজ করত (ভাগ আগে হয়), কিন্তু "x = " + 1 + 2 ছাপে x = 12, 3 নয় — কারণ বাঁ থেকে প্রথম + একটি String পেয়ে জোড়া লাগানোর কাজ শুরু করে। (নিজের প্রজেক্টে main class সাধারণত public class Main, ফাইল Main.java — এই পাতার রানারের জন্য public বাদ রাখা হয়েছে।)
৫ · তুলনা — হ্যাঁ/না প্রশ্ন, আর ==-এর ফাঁদ
তুলনার অপারেটর ছয়টি: == (সমান), != (অসমান), <, >, <=, >=. এদের বলে comparison operator. ফল সবসময় একটি boolean — true বা false. age >= 18 একটি প্রশ্ন, আর তার উত্তর একটি মান, যা ভ্যারিয়েবলে রাখা যায়: boolean adult = age >= 18;
C থেকে আসা একটি ভুল Java নিজেই আটকে দেয়। C-তে if (x = 5) (ভুল করে এক =) compile হয় আর সবসময় সত্য হয়। Java-তে if-এর ভেতরে অবশ্যই boolean লাগে, আর x = 5-এর টাইপ int — তাই compile error. (ব্যতিক্রম: x নিজেই boolean হলে if (done = true) compile হয়ে যায় — সাবধান।)
Primitive-এ মান, object-এ ঠিকানা
পর্ব ০৫-এর stack আর heap-এর ছবি মনে করুন। একটি int ভ্যারিয়েবলে সরাসরি সংখ্যাটি থাকে; একটি String ভ্যারিয়েবলে থাকে শুধু একটি reference — heap-এ রাখা object-এর ঠিকানা। == সবসময় ভ্যারিয়েবলের ভেতরে যা আছে, সেটাই তুলনা করে। তাই:
- Primitive-এ (
int,double,char,boolean…)==মান তুলনা করে।5 == 5→true. - Object-এ (
String,Integer, আপনার নিজের class…)==দেখে দুটি reference একই object দেখায় কি না — একে বলে reference equality. ভেতরের লেখা এক হলেও দুটি আলাদা object হলে ফলfalse.
দুটি String-এর লেখা এক কি না জানতে লিখুন a.equals(b). equals method ভেতরের বিষয়বস্তু মেলায়। কেন "hi" == "hi" কখনো true আর কখনো false আসে — তার কারণ String pool, বিস্তারিত পর্ব ০৮-এ। আর নিজের class-এ equals কীভাবে লিখবেন, পর্ব ২৩-এ।
সবচেয়ে ধূর্ত উদাহরণ: Integer x = 127, y = 127; হলে x == y → true, কিন্তু Integer p = 128, q = 128; হলে p == q → false! কারণ Java −১২৮ থেকে ১২৭ পর্যন্ত Integer object আগে থেকে বানিয়ে রাখে আর বারবার সেগুলোই দেয় (cache); ১২৮ হলে প্রতিবার নতুন object. ছোট সংখ্যা দিয়ে টেস্ট করলে বাগ লুকিয়ে থাকে, বাস্তবে বড় সংখ্যা এলে ধরা পড়ে। নিয়ম: wrapper (Integer, Long…) তুলনায় .equals(), অথবা আগে int-এ রূপান্তর।
দশমিক সংখ্যা == দিয়ে মেলাবেন না
0.1 + 0.2 == 0.3 Java-তে false. কারণ floating-point সংখ্যা binary-তে রাখা হয়, আর ০.১-এর মতো অনেক দশমিক সংখ্যা binary-তে ঠিকঠাক লেখা যায় না — যেমন দশমিকে ১/৩ = ০.৩৩৩… শেষ হয় না। ফলে 0.1 + 0.2 আসলে 0.30000000000000004. দশমিক তুলনায় একটি ছোট সহনসীমা (tolerance) রাখুন:
double a = 0.1 + 0.2;
boolean same = Math.abs(a - 0.3) < 1e-9; // true — পার্থক্য খুব ছোট হলেই "সমান"সহনসীমা কত হবে, তা নির্ভর করে আপনার সংখ্যা কত বড় আর কতটা নির্ভুলতা দরকার তার ওপর। আর টাকার হিসাবে double-ই এড়িয়ে চলুন: পয়সায় long রাখুন, বা BigDecimal ব্যবহার করুন (পর্ব ০৬)।
৬ · যুক্তি — &&, ||, ! আর short-circuit
বাস্তবের সিদ্ধান্ত প্রায়ই একাধিক শর্তে হয়। একটি অনলাইন দোকান ঠিক করল: “ছাড় পাবেন যদি আপনার কাছে বৈধ কুপন থাকে, অথবা আপনি member হন এবং কেনাকাটা ২০০০ টাকা বা তার বেশি হয়।” এই বাক্যকে কোডে আনতে লাগে logical operator:
| অপারেটর | মানে | কখন true |
|---|---|---|
&& | এবং (AND) | দুই দিকই true হলে |
|| | অথবা (OR) | অন্তত এক দিক true হলে |
! | না (NOT) | মানটি false হলে (উল্টে দেয়) |
boolean discount = hasCoupon || (member && total >= 2000);এখানে বন্ধনী আসলে দরকার নেই — &&-এর precedence ||-এর চেয়ে বেশি, তাই Java এমনিতেই এভাবে পড়ত। তবু বন্ধনী দিলে পাঠক এক নজরে নিয়মটি বোঝেন, নিয়ম মনে রাখার দরকার পড়ে না।
Short-circuit — দরকার না হলে ডান দিক চলেই না
false && (যা-ই হোক) সবসময় false; true || (যা-ই হোক) সবসময় true. তাই বাঁ দিক দেখেই উত্তর জানা গেলে Java ডান দিকটি চালায়ই না। একে বলে short-circuit evaluation. এটি শুধু গতির ব্যাপার নয় — এর ওপর নিরাপত্তা দাঁড়িয়ে আছে। নিচের stepper-এ s-এ কোনো String নেই, আছে null:
1String s = null;2if (s != null && s.length() > 3) {3 System.out.println("লম্বা নাম");4}5System.out.println("শেষ");
| s | |
| বাঁ দিক: s != null | |
| ডান দিক: s.length() > 3 | |
| পুরো শর্ত |
- শুরু।
sকোনো String object দেখায় না — তার মানnull. এমন হতেই পারে, যেমন ব্যবহারকারী একটি ফর্মের ঘর ফাঁকা রাখলেন। - বাঁ দিক আগে। Java সবসময় বাঁ দিক আগে হিসাব করে:
s != null→false. পুরো শর্তের উত্তর এখনো জানা নেই, এমন মনে হতে পারে। - Short-circuit: ডান দিক বাদ। কিন্তু
&&জানে, এক দিকfalseহলেই পুরোটাfalse. তাইs.length()কখনো চলে না। চললে কী হতো?null-এর ওপর method ডাকায় আসতNullPointerException, আর প্রোগ্রাম থেমে যেত। if-এর ভেতর বাদ। শর্তfalse, তাই “লম্বা নাম” ছাপা হলো না। কোনো exception নেই, কোনো ক্র্যাশ নেই।- প্রোগ্রাম স্বাভাবিকভাবে চলল। “শেষ” ছাপা হলো। এখন ভাবুন, কেউ
&&-এর জায়গায় একক&লিখলেন:&দুই দিকই সবসময় চালায়, তাইs.length()চলত আর প্রোগ্রামNullPointerException-এ থেমে যেত। আরেকটি শিক্ষা: ক্রম উল্টেs.length() > 3 && s != nullলিখলেও একই ক্র্যাশ — পাহারাদার শর্তটি সবসময় বাঁ দিকে বসান।
||-এর বেলায় উল্টো: বাঁ দিক true হলে ডান দিক চলে না। যেমন s == null || s.isEmpty() — “ফাঁকা বা না-থাকা” যাচাইয়ের প্রচলিত ধরন।
একক & আর | boolean-এর ওপরও চলে, কিন্তু short-circuit করে না — দুই দিক সবসময় হিসাব হয়। খুব কম ক্ষেত্রে এটি ইচ্ছাকৃত হয়, যেমন দুই দিকেই এমন method আছে যার চলাটা জরুরি। তবে এমন কোড বিভ্রান্তিকর; সাধারণ নিয়ম: শর্ত জুড়তে সবসময় && আর ||. আরেকটি কম-পরিচিত অপারেটর ^ (XOR): দুই দিক আলাদা হলে true.
Ternary — এক লাইনে “যদি… তবে… নইলে…”
একটি মেট্রো রেলের টিকিট-অ্যাপ ধরুন, যেখানে ১২ বছরের কম বয়সীদের টিকিট ৫০ টাকা, বাকিদের ১০০ টাকা। ternary operator শর্ত ? মান১ : মান২ দিয়ে এক লাইনেই:
int price = age < 12 ? 50 : 100;শর্ত সত্য হলে প্রথম মান, নইলে দ্বিতীয় মান। এটিই Java-র একমাত্র অপারেটর যা তিনটি operand নেয় — নাম “ternary” সেখান থেকে। এটি একটি expression, তাই সরাসরি ভ্যারিয়েবলে রাখা বা println-এ দেওয়া যায়, আর short-circuit-এর মতো যে দিকটি দরকার শুধু সেটিই হিসাব হয়। ছোট, পরিষ্কার পছন্দের জন্য এটি চমৎকার। কিন্তু একাধিক ternary একটার ভেতরে আরেকটা বসালে (a ? b : c ? d : e) কোড পড়া কঠিন হয়ে যায় — সেখানে if/else বা Java 14 থেকে আসা switch expression ভালো (পর্ব ১০)।
৭ · Bitwise অপারেটর — একটি সংখ্যায় অনেকগুলো হ্যাঁ/না
কম্পিউটারের ভেতরে প্রতিটি int আসলে ৩২টি bit — ৩২টি ছোট সুইচ, প্রতিটি ০ বা ১. Bitwise operator এই সুইচগুলোর ওপর সরাসরি কাজ করে, প্রতিটি অবস্থানে আলাদাভাবে:
| অপারেটর | কাজ | উদাহরণ (৪ bit দেখানো) |
|---|---|---|
& AND | দুটিতেই ১ হলে ১ | 0110 & 0011 = 0010 |
| OR | যেকোনোটিতে ১ হলে ১ | 0110 | 0011 = 0111 |
^ XOR | দুটি আলাদা হলে ১ | 0110 ^ 0011 = 0101 |
~ NOT | সব bit উল্টে দেয় | ~5 = -6 |
<< | বাঁয়ে সরায়, ডানে ০ ভরে (প্রতি ঘর = ×২) | 3 << 2 = 12 |
>> | ডানে সরায়, বাঁয়ে চিহ্নের bit ভরে | -16 >> 2 = -4 |
>>> | ডানে সরায়, বাঁয়ে সবসময় ০ ভরে | -16 >>> 28 = 15 |
বাস্তব উদাহরণ: ব্যবহারকারীর অনুমতি
একটি অফিসের ফাইল-শেয়ারিং অ্যাপে প্রত্যেক ব্যবহারকারীর তিন ধরনের অনুমতি থাকতে পারে: পড়া, লেখা, মোছা। তিনটি আলাদা boolean না রেখে একটি int-এর তিনটি bit ব্যবহার করা যায় — প্রতিটি অনুমতির জন্য একটি bit. একে বলে bitmask বা flag. Linux-এর ফাইল permission (chmod 755) ঠিক এই ধারণায় চলে।
- অনুমতি দেওয়া:
perm |= DELETE;— OR দিয়ে নির্দিষ্ট bit-টি ১ করা। - অনুমতি আছে কি না:
(perm & WRITE) != 0— AND দিয়ে শুধু ওই bit-টি দেখা। বন্ধনী জরুরি — কারণ নিচে অংশ ৮-এ। - অনুমতি কেড়ে নেওয়া:
perm &= ~WRITE;—~WRITE-এ WRITE ছাড়া সব bit ১, তাই AND করলে শুধু WRITE মুছে যায়। - উল্টে দেওয়া:
perm ^= READ;— থাকলে মুছবে, না থাকলে দেবে।
একটি int-এ এভাবে ৩২টি পর্যন্ত হ্যাঁ/না রাখা যায়, তুলনা করা যায় এক নির্দেশে। Java-র নিজের library-তেও এটি আছে — যেমন AWT-এর font style (Font.BOLD | Font.ITALIC). তবে সাধারণ ব্যবসায়িক কোডে আধুনিক Java-তে EnumSet বেশি পাঠযোগ্য (পর্ব ২৪)।
>> বনাম >>> — Java-র নিজস্ব অপারেটর
Java-র int সবসময় চিহ্নযুক্ত (signed): বাঁ দিকের প্রথম bit ১ হলে সংখ্যাটি ঋণাত্মক। ঋণাত্মক সংখ্যা রাখার এই পদ্ধতির নাম two's complement. ডানে সরানোর সময় বাঁ দিকে যে ফাঁকা ঘর তৈরি হয়, তা কী দিয়ে ভরা হবে — এখানেই দুই shift operator-এর পার্থক্য:
1int n = -16;2int a = n >> 2; // -43int b = n >>> 2; // 10737418204int c = n >>> 28; // 15
| bit | |
| দশমিকে |
-16-এর bit. Two's complement-এ −১৬ হলো ২৮টি ১, তারপর চারটি ০. বাঁয়ের প্রথম bit ১ — এটিই বলে সংখ্যাটি ঋণাত্মক।>> 2— চিহ্ন রক্ষা। সব bit দুই ঘর ডানে সরল, ডানের দুটি ০ বাদ পড়ল। বাঁয়ের ফাঁকা দুই ঘরে বসল চিহ্নের bit, অর্থাৎ ১. ফল −৪ — ঋণাত্মক রইল, আর মান হলো −১৬ ÷ ৪. তাই>>-কে বলে arithmetic shift: এটি ২-এর ঘাত দিয়ে ভাগের মতো কাজ করে।>>> 2— সবসময় ০. একই সরানো, কিন্তু বাঁয়ে ঢুকল ০. প্রথম bit এখন ০, তাই সংখ্যাটি হঠাৎ বিশাল ধনাত্মক: ১০৭৩৭৪১৮২০.>>>bit-গুলোকে চিহ্নহীন নকশা হিসেবে দেখে — একে বলে unsigned right shift, আর C/C++-এ এই অপারেটর নেই।>>> 28— ওপরের bit বের করা। ২৮ ঘর সরালে শুধু মূল সংখ্যার বাঁয়ের ৪টি bit (1111) ডান প্রান্তে আসে, বাকি সব ০ — ফল ১৫. hash বা checksum-এর হিসাবে, কিংবা একটিint-কে চিহ্নহীন ধরে byte-এ ভাঙতে ঠিক এটাই দরকার।>>দিয়ে করলে পাওয়া যেত −১, কারণ বাঁয়ে ১ ভরতে থাকত।
একটি খুঁটিনাটি: shift-এর পরিমাণ int-এর জন্য ৩২ দিয়ে ভাগশেষ নিয়ে ধরা হয়। তাই 1 << 33 আসলে 1 << 1 = 2, শূন্য নয়। long-এর বেলায় ৬৪ দিয়ে। এবার অনুমতি, ternary, short-circuit আর Math একসাথে চালিয়ে দেখুন:
class Main {
static final int READ = 1, WRITE = 2, DELETE = 4; // 001, 010, 100
public static void main(String[] args) {
String coupon = null;
boolean hasCoupon = coupon != null && coupon.length() > 3; // NPE হয় না
int total = 2400; boolean member = true;
boolean discount = hasCoupon || (member && total >= 2000);
System.out.println("কুপন? " + hasCoupon + ", ছাড়? " + discount);
int age = 10;
System.out.println("টিকিট: " + (age < 12 ? 50 : 100) + " টাকা");
int perm = READ | WRITE; // 011
System.out.println("লিখবে? " + ((perm & WRITE) != 0) + ", মুছবে? " + ((perm & DELETE) != 0));
perm |= DELETE; perm &= ~WRITE; // 101
System.out.println("নতুন perm = " + Integer.toBinaryString(perm));
System.out.println("-16 >> 2 = " + (-16 >> 2) + ", -16 >>> 28 = " + (-16 >>> 28));
double a = 0.1 + 0.2;
System.out.println((a == 0.3) + " " + (Math.abs(a - 0.3) < 1e-9));
System.out.println(Math.round(2.5) + " " + Math.pow(2, 10) + " " + Math.max(3, 7));
}
}আউটপুট: কুপন? false, ছাড়? true — কুপন null হলেও কোনো exception আসেনি, আর member হিসেবে ২৪০০ টাকার কেনাকাটায় ছাড় মিলেছে। তারপর টিকিট: 50 টাকা; লিখবে? true, মুছবে? false; DELETE দিয়ে WRITE কেড়ে নেওয়ার পর নতুন perm = 101; -16 >> 2 = -4, -16 >>> 28 = 15; দশমিক তুলনায় false true (== ব্যর্থ, সহনসীমা সফল); আর শেষ লাইনে 3 1024.0 7. লক্ষ করুন Math.pow সবসময় double দেয় (1024.0), আর Math.round(2.5) দেয় 3 — যার টাইপ long, পরের অংশে দেখবেন।
৮ · কে আগে? precedence, ক্রম আর Math class
স্কুলে শিখেছেন “ভাগ-গুণ আগে, যোগ-বিয়োগ পরে”। Java-রও এমন নিয়ম আছে, তবে অনেক বেশি অপারেটরের জন্য। কোন অপারেটর আগে operand ধরবে — তাকে বলে operator precedence. একই স্তরের অপারেটর পাশাপাশি থাকলে কোন দিক থেকে ধরা হবে — তাকে বলে associativity; বেশিরভাগ অপারেটর বাঁ থেকে ডানে, কিন্তু assignment (=, +=) আর ternary ডান থেকে বাঁয়ে (a = b = 0 মানে a = (b = 0)). ধাপে ধাপে দেখুন:
1int a = 10, b = 4, c = 3, d = 2, e = 4;2int r = a + b * c / d % e;3System.out.println(r);
| এখন | |
| এইমাত্র হলো |
- মান বসান। ভ্যারিয়েবলের জায়গায় মান বসালে দাঁড়ায়
10 + 4 * 3 / 2 % 4. এখানে দুই স্তরের অপারেটর:* / %(উঁচু স্তর) আর+(নিচু স্তর)। *প্রথমে — কারণ বাঁয়ে।*,/,%একই স্তরের। একই স্তরে বাঁ থেকে ডানে, তাই প্রথম4 * 3 = 12. অনেকে ভাবেন “ভাগ আগে” — না, এই তিনটির মধ্যে যে বাঁয়ে, সে আগে।- তারপর
/.12 / 2 = 6. এখানে ভাগ নিখুঁত, কিন্তু মনে রাখুন এটি integer division —13 / 2হলে হতো6,6.5নয়। - তারপর
%.6 % 4 = 2. উঁচু স্তরের সব কাজ শেষ, বাকি শুধু যোগ। - সবশেষে
+.10 + 2 = 12. বন্ধনী দিলে উদ্দেশ্য অনেক স্পষ্ট হতো:a + ((b * c / d) % e). আর যদি আপনি আসলে(a + b) * cচাইতেন, বন্ধনী ছাড়া কোনোভাবেই হতো না।
Java-র পুরো precedence টেবিলে পনেরোর বেশি স্তর। মুখস্থ করার দরকার নেই — নিচের সরল রূপটি জানলেই যথেষ্ট, বাকিটা বন্ধনী দিয়ে সামলান:
| স্তর (ওপরে = আগে) | অপারেটর | দিক |
|---|---|---|
| ১ · postfix | i++ i-- | — |
| ২ · unary | ++i --i -x ! ~ (type) | ডান → বাঁ |
| ৩ · গুণ-ভাগ | * / % | বাঁ → ডান |
| ৪ · যোগ-বিয়োগ | + - | বাঁ → ডান |
| ৫ · shift | << >> >>> | বাঁ → ডান |
| ৬ · তুলনা | < > <= >= instanceof | বাঁ → ডান |
| ৭ · সমতা | == != | বাঁ → ডান |
| ৮ · bitwise | &, তারপর ^, তারপর | | বাঁ → ডান |
| ৯ · logical | &&, তারপর || | বাঁ → ডান |
| ১০ · ternary | ? : | ডান → বাঁ |
| ১১ · assignment | = += -= &= … | ডান → বাঁ |
টেবিলের একটি ফাঁদ চোখে রাখুন: bitwise & আছে ==-এর নিচে। তাই perm & WRITE != 0 Java পড়ে perm & (WRITE != 0) হিসেবে — যা int & boolean, compile error. C-তে একই ভুল চুপচাপ compile হয়ে ভুল ফল দেয়; Java-র কঠোর টাইপ অন্তত এখানে আপনাকে বাঁচায়। সমাধান সেই একই — বন্ধনী: (perm & WRITE) != 0.
Precedence এক জিনিস, মূল্যায়নের ক্রম আরেক
Precedence বলে কোন অপারেটর কোন operand ধরবে — expression-এর গঠন। কিন্তু operand-গুলো কোন ক্রমে হিসাব হবে, সেটি আলাদা প্রশ্ন। Java-র উত্তর পরিষ্কার: evaluation order সবসময় বাঁ থেকে ডানে (Java Language Specification §15.7). f() + g() * h()-এ গুণ আগে হবে ঠিকই, কিন্তু তিনটি method ডাকা হবে f, g, h ক্রমে। Method-এর argument-ও বাঁ থেকে ডানে হিসাব হয়। এই নিশ্চয়তার জন্যই অংশ ৪-এর i = i++ সব JVM-এ একই ফল দেয়। তবু JLS নিজেই পরামর্শ দেয়: কোড যেন এই নিয়মের ওপর খুব বেশি নির্ভর না করে — একটি expression-এ একটির বেশি side effect রাখবেন না।
Math class — হাতের কাছের হিসাব-যন্ত্র
অপারেটরে যা নেই, তার জন্য আছে java.lang.Math — import ছাড়াই ব্যবহার করা যায়। এর সব method static, তাই Math.max(3, 7)-এর মতো সরাসরি ডাকা যায় (পর্ব ১৮-এ static-এর বিস্তারিত)।
| Method | কাজ | উদাহরণ ও সতর্কতা |
|---|---|---|
Math.max(a, b), Math.min(a, b) | বড়/ছোটটি | Math.max(3, 7) → 7 |
Math.abs(x) | পরম মান | Math.abs(-9) → 9; কিন্তু Math.abs(Integer.MIN_VALUE) ঋণাত্মকই থাকে (overflow) |
Math.pow(a, b) | ঘাত | সবসময় double: Math.pow(2, 10) → 1024.0 |
Math.sqrt(x) | বর্গমূল | Math.sqrt(2) → 1.4142135623730951; ঋণাত্মকে NaN |
Math.round(x) | নিকটতম পূর্ণসংখ্যা | double দিলে long ফেরত! Math.round(2.5) → 3, Math.round(-2.5) → -2 |
Math.addExact, Math.multiplyExact | overflow হলে exception | টাকা, পরিমাণ — যেখানে ভুল ফল চলবে না |
Math.floorDiv, Math.floorMod | নিচের দিকে কাটা ভাগ | ঘড়ি, ঘুরে আসা index |
Math.round-এর টাইপ নিয়ে সবচেয়ে বেশি হোঁচট খান নতুনরা: int n = Math.round(4.6); compile হয় না, কারণ double দিলে ফল long, আর long থেকে int-এ মান হারাতে পারে — সেই পুরোনো “possible lossy conversion” বার্তা। হয় long n লিখুন, নয়তো (int) Math.round(4.6). (float দিলে অবশ্য int ফেরত দেয়।) আর Math.round .৫-কে সবসময় ওপরের দিকে (ধনাত্মক অসীমের দিকে) নেয় — তাই −২.৫ হয় −২, −৩ নয়।
&আর&&-এর পার্থক্য? — দুটিই boolean-এ AND, কিন্তু&&short-circuit করে (বাঁ দিকfalseহলে ডান দিক চলে না);&দুই দিক সবসময় চালায়, আর পূর্ণসংখ্যায় bitwise AND.==আরequals()-এর পার্থক্য? — object-এ==reference (একই object কি না) তুলনা করে;equals()বিষয়বস্তু তুলনা করে (যদি class সেভাবে লিখে থাকে, যেমন String).>>আর>>>-এর পার্থক্য? —>>বাঁয়ে চিহ্নের bit ভরে (ঋণাত্মক থাকে),>>>সবসময় ০ ভরে।byte b = 10; b = b + 1;কেন compile হয় না, অথচb += 1;হয়? —b + 1-এর টাইপint; compound assignment-এ লুকানো cast আছে।int i = 5; i = i++;-এর পরi? — ৫; পুরোনো মান রেখে দেওয়া হয়,iবাড়ে, তারপর পুরোনো মান আবারi-তে লেখা হয়।
৯ · AI-এর যুগে — অপারেটরের নিঃশব্দ ভুল ধরা
AI-এর লেখা Java কোড সাধারণত compile হয়, চলেও। অপারেটরের ভুল তাই চোখে পড়ে না — কোনো লাল দাগ নেই, শুধু কিছু ক্ষেত্রে ভুল উত্তর। AI ভুলগুলো শেখে ইন্টারনেটের লাখো কোড থেকে, যেখানে এগুলো খুব প্রচলিত। আর Python ও JavaScript-এর কোড থেকে শেখা অভ্যাস সে কখনো কখনো Java-তে নিয়ে আসে, যেখানে নিয়ম আলাদা।
| AI-এর কোডে যা দেখবেন | সমস্যা | ঠিক রূপ |
|---|---|---|
if (role == "admin") | reference তুলনা; টেস্টে কাজ করে, বাস্তব ইনপুটে ব্যর্থ | "admin".equals(role) |
double avg = sum / count; | দুটিই int হলে ভগ্নাংশ হারায় | (double) sum / count |
if (s != null & s.isEmpty()) | একক & — null হলে NPE | && |
if (n % 2 == 1) | ঋণাত্মক বিজোড়ে -1 আসে, ভুল | n % 2 != 0 |
int ms = days * 24 * 60 * 60 * 1000; | ২৫ দিন বা তার বেশি হলে overflow | long, 24L, বা Math.multiplyExact |
a || b && c, x & MASK == 0 | precedence অনুমানের ওপর নির্ভর | স্পষ্ট বন্ধনী |
"admin".equals(role) লেখার কৌশলটি খেয়াল করুন: ধ্রুব লেখাটি বাঁ দিকে রাখলে role null হলেও NPE আসে না, শুধু false আসে। দুই দিকই null হতে পারলে java.util.Objects.equals(a, b).
AI-কে অপারেটর-সংক্রান্ত কোড লিখতে বা যাচাই করতে বললে নিয়মগুলো আগেই বেঁধে দিন:
AI-এর লেখা কোড হাতে পেলে — অপারেটর চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Java-তে -7 % 2 কেন -1, অথচ Python-এ 1? কোনটি “ঠিক”?
গণিতে দুটি উত্তরই বৈধ — প্রশ্ন শুধু ভাগফল কোন দিকে গোল করা হবে। -7 / 2 = −৩.৫. শূন্যের দিকে কাটলে −৩ (truncation), নিচের দিকে কাটলে −৪ (floor). ভাগশেষ সবসময় a - (a / b) * b, তাই প্রথম পথে −১, দ্বিতীয় পথে ১. আসল প্রশ্ন: কোন ভাষা কোন পথ নিল, আর কেন।
Java-র পথ এসেছে ইতিহাস আর হার্ডওয়্যার থেকে। Fortran বহু আগে থেকেই শূন্যের দিকে কাটত, আর বেশিরভাগ CPU-র ভাগের নির্দেশও তাই করে — যেমন x86-এর IDIV. প্রথম দিকের C (১৯৮৯ সালের ANSI মান) ঋণাত্মক ভাগের দিক ঠিক করে দেয়নি; compiler নিজের মতো বাছতে পারত, ফলে একই C কোড দুই মেশিনে দুই উত্তর দিতে পারত। Java ১৯৯৫ সালে শুরু থেকেই নিয়ম বেঁধে দেয়: শূন্যের দিকে কাটা। এতে হার্ডওয়্যারের নির্দেশ সরাসরি ব্যবহার করা যায়, বাড়তি কাজ নেই, আর C প্রোগ্রামারদের অভ্যাসের সাথে মেলে। পরে C99 মানও একই নিয়ম বাধ্যতামূলক করে, C++11-ও।
Python-এর স্রষ্টা Guido van Rossum ২০১০ সালে একটি ব্লগ লেখায় (“Why Python's Integer Division Floors”) অন্য পথের যুক্তি দেন: ভাগশেষের চিহ্ন যদি ভাজকের মতো হয়, তাহলে a % b সবসময় ০ থেকে b - 1-এর মধ্যে থাকে। ঘড়ির হিসাব, তারিখ, ঘুরে আসা index — এসব কাজে ঠিক এটাই দরকার। যেমন একটি Unix timestamp ঋণাত্মক হলেও (১৯৭০-এর আগের সময়) দিনের ঘণ্টা বের করার সূত্র একই থাকে। Python তাই কিছুটা গতির দাম দিয়ে গাণিতিক সুবিধা বেছে নিয়েছে।
Java এই প্রয়োজন অস্বীকার করেনি, শুধু দেরিতে সমাধান দিয়েছে: Java 8-এ আসে Math.floorDiv আর Math.floorMod. অর্থাৎ আজ Java-তে দুটি আচরণই আছে — অপারেটরে হার্ডওয়্যারের পথ, library-তে গণিতের পথ। আপনার দায়িত্ব জানা, কখন কোনটি দরকার। ঋণাত্মক সংখ্যা আসতে পারে এমন জায়গায় % দেখলেই নিজেকে প্রশ্ন করুন: “এখানে −১ এলে কি চলবে?”
মূল উপলব্ধি: ভাষার “অদ্ভুত” আচরণের পেছনে প্রায়ই একটি ইচ্ছাকৃত বিনিময় থাকে — এখানে হার্ডওয়্যারের গতি ও C-র সাথে মিল বনাম গাণিতিক সৌন্দর্য। নিয়মটা জানলে অবাক হওয়ার কিছু থাকে না, শুধু সঠিক টুল বাছতে হয়।
প্র ০২C-তে >>> নেই, Java-তে কেন আছে? Java-তে unsigned টাইপ না থাকার সাথে এর সম্পর্ক কী?
C-তে int-এর পাশাপাশি unsigned int আছে। unsigned সংখ্যায় ডানে সরালে বাঁয়ে সবসময় ০ ঢোকে, আর signed-এ কী ঢুকবে তা ঐতিহাসিকভাবে compiler-এর ওপর ছেড়ে দেওয়া ছিল। তাই C-র একটি >>-ই দুই কাজ করে — কোনটি, তা ঠিক হয় operand-এর টাইপ দেখে।
James Gosling Java নকশার সময় unsigned পূর্ণসংখ্যার টাইপ ইচ্ছা করে বাদ দেন। তিনি পরে এক সাক্ষাৎকারে বলেছিলেন, তাঁর অভিজ্ঞতায় খুব কম C প্রোগ্রামারই আসলে বোঝেন unsigned হিসাবে কী ঘটে — signed আর unsigned মিশালে C-র নিয়ম এতটাই জটিল যে সূক্ষ্ম বাগ তৈরি হয়। যেমন C-তে -1 < 1u মিথ্যা হয়, কারণ −১ unsigned-এ রূপান্তরিত হয়ে বিশাল সংখ্যা হয়ে যায়। Java-র লক্ষ্য ছিল সরলতা: byte, short, int, long সবাই signed, আকার সব প্ল্যাটফর্মে নির্দিষ্ট। একমাত্র ব্যতিক্রম char, যা ১৬-bit unsigned — কারণ এটি সংখ্যা নয়, Unicode অক্ষরের কোড।
কিন্তু bit নিয়ে কাজ করার দরকার তো থেকেই যায় — hash function, checksum, নেটওয়ার্ক প্যাকেট, ছবি বা অডিওর byte, এনক্রিপশন। এসব ক্ষেত্রে সংখ্যাটির “মান” নয়, bit-এর নকশাটাই আসল। unsigned টাইপ না থাকায় Java আলাদা অপারেটর দিল: >> সবসময় চিহ্ন রাখে, >>> সবসময় ০ ঢোকায়। টাইপ নয়, অপারেটর বলে দেয় আপনি সংখ্যাটিকে কীভাবে দেখছেন। Java-র নিজের HashMap-এর ভেতরেই এর ব্যবহার আছে: hash মানের ওপরের bit-গুলো নিচে মেশাতে h ^ (h >>> 16).
এই নকশার দামও আছে। byte-কে চিহ্নহীন সংখ্যা হিসেবে পড়তে প্রতিবার b & 0xFF লিখতে হয়, যা নতুনদের কাছে রহস্যময়। আর >>> byte বা short-এ ব্যবহার করলে আগে numeric promotion-এ int হয় — তাই ঋণাত্মক byte-এ ফল প্রায়ই প্রত্যাশামতো হয় না। চাহিদা মেটাতে Java 8-এ আসে Integer.toUnsignedString, Integer.divideUnsigned, Integer.compareUnsigned-এর মতো method — টাইপ নয়, হিসাবের পদ্ধতি দিয়ে unsigned সমর্থন।
মূল উপলব্ধি: একটি জটিলতা বাদ দিলে সেটি পুরোপুরি হারায় না, অন্য কোথাও সরে যায়। Java টাইপের জটিলতা সরিয়ে অপারেটর আর library-তে নিয়ে গেছে — যাতে বেশিরভাগ প্রোগ্রামার কখনো এর মুখোমুখি না হন, আর যাঁদের দরকার, তাঁরা স্পষ্টভাবে চেয়ে নেন।
প্র ০৩Short-circuit evaluation-এর ধারণা কোথা থেকে এলো? C-তে & আর && আলাদা হলো কেন?
ধারণাটির শিকড় John McCarthy-র কাজে। ১৯৫০-এর দশকের শেষে Lisp তৈরির সময় McCarthy “conditional expression” প্রস্তাব করেন — যদি p তবে a, নইলে b — যা একটি মান দেয়, আর যে শাখা দরকার নেই সেটি হিসাবই হয় না। ১৯৬০ সালের বিখ্যাত প্রবন্ধে তিনি “এবং” আর “অথবা”-কে এই conditional expression দিয়েই সংজ্ঞায়িত করেন: “p এবং q” মানে “যদি p তবে q, নইলে মিথ্যা”। ফলে p মিথ্যা হলে q দেখাই হয় না। এজন্য short-circuit মূল্যায়নকে কোথাও কোথাও “McCarthy evaluation”-ও বলা হয়। আজকের ternary ?:-ও এই conditional expression-এর বংশধর।
সব ভাষা এটি নেয়নি। Pascal-এর মান and/or-এর দুই দিক হিসাব হবে কি না, তা নির্দিষ্ট করেনি — ফলে (p <> nil) and (p^.value > 0)-এর মতো কোড কোনো compiler-এ নিরাপদ, কোনোটিতে ক্র্যাশ। Pascal প্রোগ্রামারদের তাই নেস্টেড if লিখতে হতো।
C-র গল্পটি আরও মজার। Dennis Ritchie তাঁর “The Development of the C Language” প্রবন্ধে লিখেছেন, পূর্বসূরি B-তে এবং শুরুর C-তে একই & আর | দুই কাজ করত — if-এর শর্তে short-circuit, সাধারণ হিসাবে bitwise. এই দ্বৈত অর্থ বিভ্রান্তিকর ছিল, তাই C-তে আলাদা && আর || যোগ হয়। কিন্তু পুরোনো কোড যেন ভেঙে না যায়, সেজন্য &-এর precedence আগের জায়গায় — ==-এর নিচে — রেখে দেওয়া হয়। Ritchie পরে লিখেছেন, পেছনে তাকালে মনে হয় &-এর precedence বদলে দেওয়াই ভালো হতো। সেই কারণেই x & MASK == 0 আসলে x & (MASK == 0).
Java এই precedence টেবিল C থেকে হুবহু নিয়েছে, ফাঁদসহ — C প্রোগ্রামারদের পরিচিত লাগবে বলে। কিন্তু Java-তে boolean আর int আলাদা টাইপ, একটি আরেকটিতে আপনাআপনি রূপান্তর হয় না। তাই x & (MASK == 0) Java-তে compile error — উত্তরাধিকারের ভুলটি টাইপ-ব্যবস্থা ধরে ফেলে। আর Java boolean-এ একক &/| রেখেছে স্পষ্ট অর্থে: “দুই দিকই চালাও”।
মূল উপলব্ধি: আজকের প্রতিটি “স্বাভাবিক” অপারেটরের পেছনে ৬০ বছরের সিদ্ধান্ত আর আপস জমে আছে। পুরোনো কোডের সাথে মিল রাখার চাপ কখনো কখনো একটি নকশার ভুলকে প্রজন্মের পর প্রজন্ম বাঁচিয়ে রাখে — আর ভালো টাইপ-ব্যবস্থা সেই ভুলের ক্ষতি কমাতে পারে।
প্র ০৪C-তে i = i++ undefined behaviour, Java-তে নির্দিষ্টভাবে ৫ — Java কেন মূল্যায়নের ক্রম বেঁধে দিল?
C-র মান বেশিরভাগ ক্ষেত্রে বলে না, একটি অপারেটরের operand-গুলো কোন ক্রমে হিসাব হবে। f() + g()-এ কোনটি আগে ডাকা হবে, compiler নিজের সুবিধামতো ঠিক করে। আর একই ভ্যারিয়েবলকে দুই “sequence point”-এর মধ্যে দুবার বদলালে — যেমন i = i++ — আচরণ undefined: মান যা খুশি হতে পারে, আর অপ্টিমাইজার এমন কোডকে “ঘটবেই না” ধরে নিয়ে অদ্ভুত রূপান্তর করতে পারে। কারণ ছিল গতি: ক্রম না বাঁধলে compiler register-এর ব্যবহার আর নির্দেশের ক্রম অবাধে সাজাতে পারে, যা ১৯৭০-৮০-এর দুর্বল মেশিনে বড় সুবিধা ছিল।
Java-র অগ্রাধিকার ছিল ভিন্ন: “Write once, run anywhere”. একই bytecode যদি দুই JVM-এ দুই উত্তর দেয়, তাহলে স্লোগানটির মানে থাকে না। তাই Java Language Specification-এর §15.7 (“Evaluation Order”) স্পষ্ট নিয়ম দেয়: binary অপারেটরের বাঁ operand পুরোপুরি হিসাব হয় ডান operand-এর আগে; method-এর argument বাঁ থেকে ডানে; compound assignment-এ বাঁ দিকের মান আগে সংরক্ষিত হয়। ফলে i = i++ সবসময় ৫, x = i++ + ++i (i = ১ দিয়ে শুরু) সবসময় ৪. একই দর্শন থেকে Java int-এর আকার সব প্ল্যাটফর্মে ৩২ bit বেঁধে দিয়েছে, আর Java 17 থেকে (JEP 306) সব floating-point হিসাব সব মেশিনে বিট-বিট একই।
দাম কি দিতে হলো? তত্ত্বে কিছু অপ্টিমাইজেশনের সুযোগ কমে। বাস্তবে JIT প্রমাণ করতে পারে যে দুটি operand একে অপরকে প্রভাবিত করে না, তখন ক্রম বদলাতে বাধা নেই — কারণ ফল একই থাকে। তাই গতির ক্ষতি সামান্য।
মজার ব্যাপার, পরে C++ নিজেও এই দিকে কিছুটা সরে এসেছে: C++17-এ কিছু ক্ষেত্রে ক্রম নির্দিষ্ট করা হয় — যেমন <<-এর বাঁ দিক আগে, assignment-এ ডান দিক আগে — কারণ cout << i++ << i-এর মতো কোডের অনিশ্চয়তা বছরের পর বছর বাগের উৎস ছিল।
তবু JLS নিজেই সতর্ক করে: কোড যেন এই নিয়মের ওপর খুব বেশি নির্ভর না করে, কারণ প্রতিটি expression-এ একটির বেশি side effect না থাকলে কোড সাধারণত বেশি পরিষ্কার হয়। নিয়মটি নিরাপত্তা-জাল, লেখার কৌশল নয়।
মূল উপলব্ধি: “নির্দিষ্ট আচরণ” আর “পড়তে সহজ” এক জিনিস নয়। Java কোডটিকে অনুমানযোগ্য বানিয়েছে, কিন্তু পাঠযোগ্য বানানো আপনার দায়িত্ব — এক লাইনে এক কাজ।
প্র ০৫Java-তে operator overloading নেই কেন? তাহলে String-এ + কীভাবে কাজ করে?
C++-এ নিজের class-এর জন্য অপারেটরের অর্থ বদলানো যায় — একে বলে operator overloading. Matrix a, b; হলে a + b লিখে দুটি ম্যাট্রিক্স যোগ করা যায়, যা গণিতের কোডে চমৎকার পাঠযোগ্যতা দেয়। কিন্তু এর অপব্যবহারও প্রচুর: << দিয়ে আউটপুট, + দিয়ে কোনো তালিকায় যোগ, এমনকি কোনো library-তে , অপারেটর বদলানো। কোড দেখে আর বোঝা যায় না একটি + আসলে কী করছে — হয়তো নেটওয়ার্কে কিছু পাঠাচ্ছে।
James Gosling এটি Java-তে রাখেননি, আর পরে খোলাখুলি বলেছেন, সিদ্ধান্তটি অনেকটা ব্যক্তিগত: তিনি C++-এ অনেককে এর অপব্যবহার করতে দেখেছিলেন। Java-র নকশার মূল চিন্তা ছিল — কোড যেন যে কেউ পড়ে বুঝতে পারেন, কোনো লুকানো জাদু ছাড়া। a + b দেখলে আপনি নিশ্চিত জানেন, এটি সংখ্যার যোগ বা String জোড়া — অন্য কিছু নয়।
এর একমাত্র ব্যতিক্রম ভাষার ভেতরেই বসানো: + আর += String-এর জন্য জোড়া লাগানোর কাজ করে, কারণ লেখা জোড়া এত সাধারণ যে a.concat(b).concat(c) লিখতে বাধ্য করা অসহনীয় হতো। ভেতরে javac এটিকে অন্য কোডে রূপান্তর করে — পুরোনো Java-তে StringBuilder-এর method কলে, আর Java 9 থেকে (JEP 280) invokedynamic দিয়ে JVM-কে কাজটি সবচেয়ে ভালোভাবে করার স্বাধীনতা দিয়ে। নিয়মটি হলো: +-এর যেকোনো operand String হলে অন্যটিও String-এ রূপান্তরিত হয়, আর বাঁ থেকে ডানে হিসাব হয় — তাই 1 + 2 + "3" = "33", কিন্তু "1" + 2 + 3 = "123".
দাম আছে। BigDecimal দিয়ে টাকার হিসাব লিখতে হয় price.multiply(qty).add(tax) — price * qty + tax-এর তুলনায় অনেক কম পাঠযোগ্য। বৈজ্ঞানিক ও আর্থিক কোডে এটি Java-র একটি পরিচিত অভিযোগ। JVM-এর অন্য ভাষাগুলো ভিন্ন পথ নিয়েছে: Kotlin-এ operator fun plus লিখে নিয়ন্ত্রিতভাবে overloading করা যায়, আর Kotlin-এ BigDecimal-এ সরাসরি + লেখা যায়; Scala-য় প্রায় যেকোনো চিহ্ন method-এর নাম হতে পারে।
মূল উপলব্ধি: ভাষার নকশা মানে বাদ দেওয়ার সিদ্ধান্তও। Java কিছু পাঠযোগ্যতা ছেড়েছে স্বচ্ছতার বিনিময়ে: কোডের প্রতিটি অপারেটরের অর্থ আপনি এই পর্বের নিয়ম থেকেই জানেন — কোনো class তা গোপনে বদলাতে পারে না।
অনুশীলন
প্রথমে খাতায় উত্তর লিখুন, তারপর JShell (পর্ব ০৩) বা উপরের রানারে চালিয়ে মিলিয়ে নিন।
-
হাতে হিসাব: চালানোর আগে প্রতিটির মান ও টাইপ লিখুন —
17 / 5,17 % 5,-17 % 5,Math.floorMod(-17, 5),17 / 5.0,1 + 2 + "3" + 4 + 5.17 / 5=3(int);17 % 5=2;-17 % 5=-2(চিহ্ন বাঁ operand-এর মতো, কারণ −১৭ / ৫ শূন্যের দিকে কেটে −৩, আর −১৭ − (−১৫) = −২);Math.floorMod(-17, 5)=3(নিচের দিকে কেটে −৪, আর −১৭ − (−২০) = ৩);17 / 5.0=3.4(double); শেষটি"3345"— বাঁ থেকে প্রথমে1 + 2= ৩ (সংখ্যা), তারপর String"3"পেয়ে"33", এরপর সব জোড়া লাগানো:"334","3345". -
সেকেন্ড থেকে ঘণ্টা-মিনিট (কোড লিখুন): একটি
int seconds = 4000;থেকে শুধু/আর%দিয়ে1:06:40আকারে সময় ছাপুন। মিনিট ও সেকেন্ড দুই অঙ্কে দেখাতে হবে।ঘণ্টা =
seconds / 3600= ১; বাকি =seconds % 3600= ৪০০; মিনিট =400 / 60= ৬; সেকেন্ড =400 % 60= ৪০. দুই অঙ্কে দেখাতেString.format("%d:%02d:%02d", h, m, s)(পর্ব ০৯-এ বিস্তারিত)। ternary দিয়েও করা যায়:(m < 10 ? "0" : "") + m. লক্ষ করুন, এখানে integer division-ই আসল হাতিয়ার — দশমিক ভাগ করলে উল্টো ঝামেলা হতো। -
বাগ খুঁজুন: একটি রেজাল্ট-অ্যাপে লেখা আছে
int a = 70, b = 85, c = 90; double avg = (a + b + c) / 3; if (avg >= 81.5 & grade != null && grade.equals("A")) ...— কতগুলো সমস্যা দেখছেন?এক:
(a + b + c) / 3integer division, তাইavg=81.0, আসল ৮১.৬৭ নয় — শর্তavg >= 81.5ভুলভাবে মিথ্যা হয়। সমাধান/ 3.0. দুই: একক&—&-এর precedence&&-এর চেয়ে বেশি, তাই Java পড়ে(avg >= 81.5 & grade != null) && grade.equals("A").gradenullহলে বাঁ অংশfalse, তাই এখানে ক্র্যাশ হয় না — কিন্তু এটা ভাগ্যের ব্যাপার, উদ্দেশ্য অস্পষ্ট, আর অপ্রয়োজনে দুই দিক হিসাব হয়;&&লিখুন। তিন (শৈলী):"A".equals(grade)লিখলেnull-পরীক্ষাই লাগত না। -
Permission flag: READ = ১, WRITE = ২, DELETE = ৪. একজন ব্যবহারকারীর
perm = 6. তাঁর কী কী অনুমতি আছে? DELETE কেড়ে নিতে কী লিখবেন, আর পরেperm-এর মান কত হবে? READ এবং WRITE দুটোই আছে কি না এক expression-এ কীভাবে যাচাই করবেন?৬ = binary
110= WRITE (২) + DELETE (৪); READ নেই। DELETE কাড়তেperm &= ~DELETE;→110 & ...011=010= ২ (শুধু WRITE). দুটোই আছে কি না:(perm & (READ | WRITE)) == (READ | WRITE). শুধু(perm & (READ | WRITE)) != 0লিখলে “যেকোনো একটি আছে” যাচাই হয় — এটি একটি পরিচিত ভুল। আর বন্ধনী বাদ দিলে==আগে হিসাব হয়ে compile error আসত।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJava অপারেটরগুলোকে precedence অনুযায়ী সাজান — সবচেয়ে আগে যেটি হিসাব হয় সেটি ওপরে (▲▼ চেপে সরান)
!আর unary-(একটি operand)*/%+-<><=>===!=&&
Java-তে -7 % 3-এর মান কত?
- -1
- 2
- 1
- -2
byte a = 10, b = 20; byte c = a + b; — Java-তে কী ঘটে?
- c-তে 30 থাকে, কোনো সমস্যা নেই
- চলার সময় ArithmeticException আসে
- Compile error: possible lossy conversion from int to byte
- c-তে -226 থাকে (overflow)
int i = 5; i = i++; — এর পরে Java-তে i-এর মান কত?
- 6
- 5
- Undefined — JVM ভেদে আলাদা
- Compile error
String s = null; boolean ok = s != null && s.length() > 0; — কী ঘটে?
- NullPointerException আসে
- Compile error আসে
- ok = true
- ok = false, কোনো exception নেই
Java-র কোন অপারেটর একটি int-কে ডানে সরানোর সময় বাঁ দিকের ফাঁকা bit সবসময় 0 দিয়ে ভরে, সংখ্যাটি ঋণাত্মক হলেও?
>><<>>>~
Java-তে Math.round(2.5) কোন টাইপের কোন মান ফেরত দেয়?
int, 3long, 3double, 3.0long, 2
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- দুটি পূর্ণসংখ্যার
/ভগ্নাংশ ফেলে দেয়, আর%-এর চিহ্ন বাঁ operand-এর মতো (-7 % 2 == -1); গাণিতিক ভাগশেষেMath.floorMod. byte/short/charহিসাবের আগেintহয় (numeric promotion);+=-এ লুকানো cast থাকে; overflow নিঃশব্দ — দরকারেMath.addExact.==primitive-এ মান, object-এ reference তুলনা করে; লেখা মেলাতেequals, দশমিকে সহনসীমা।&&/||short-circuit করে —null-পাহারা বাঁ দিকে;&/|/^/>>>bit নিয়ে কাজ করে।- মূল্যায়ন সবসময় বাঁ থেকে ডানে, কিন্তু precedence-এ ভরসা না করে বন্ধনী দিন; এক expression-এ একটির বেশি side effect নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৮ · String, StringBuilder আর immutabilityপরবর্তী পর্ব== বনাম equals-এর পুরো গল্প, String pool আর দ্রুত জোড়া লাগানো।
- Java প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python অপারেটর — তুলনা, যুক্তি, walrus আর is বনাম ==তুলনা করুনএকই অপারেটর, ভিন্ন নিয়ম: // আর নিচের দিকে কাটা %.
- C অপারেটর — C যেভাবে হিসাব করে, তুলনা করে আর যুক্তি সাজায়পাশের ট্র্যাকযেখানে মূল্যায়নের ক্রম অনির্দিষ্ট আর unsigned টাইপ আছে।