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

array — একই ধরনের অনেক মান, পাশাপাশি সাজানো

Java Arrays — Creation, Indexing, Copying, java.util.Arrays & 2D Arrays

Taught in Bangla, this lesson covers Java arrays: the mental model of contiguous cells on the heap, creation, default values and length, bounds checking, looping for sum, max, search and reverse, aliasing versus real copies, passing arrays to methods, java.util.Arrays helpers, 2D and jagged arrays, and array versus ArrayList.

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

  • Array-এর মানসিক ছবি — পাশাপাশি ঘর, ০ থেকে নম্বর, heap-এ একটি object
  • Array তৈরি, default value, length, আর সীমার বাইরে গেলে কী ঘটে ও কেন
  • for আর for-each দিয়ে ঘোরা; যোগফল, গড়, সর্বোচ্চ, খোঁজা, উল্টানো, গোনা
  • Reference, aliasing আর সঠিক কপি; method-এ array পাঠালে কী হয়
  • java.util.Arrays, 2D ও jagged array, আর কখন array বনাম কখন ArrayList

১ · ৪০ জনের নম্বর — ৪০টি ভ্যারিয়েবল?

ধরুন একটি ক্লাসে ৪০ জন শিক্ষার্থী, আর আপনাকে সবার পরীক্ষার নম্বর রেখে গড় বের করতে হবে। এ পর্যন্ত যা শিখেছেন, তা দিয়ে লিখতে হতো int m1, m2, m3, …, m40; — চল্লিশটি আলাদা নাম। গড় বের করতে চল্লিশটি নাম যোগ করার একটি লম্বা লাইন। কাল যদি শিক্ষার্থী হয় ৪৫ জন? পুরো কোড নতুন করে লিখতে হবে। আর পর্ব ১১-এর লুপ এখানে কোনো কাজেই আসবে না, কারণ m1, m2 নামগুলো লুপের ভ্যারিয়েবল দিয়ে বানানো যায় না।

দরকার এমন একটি জিনিস, যার একটি নাম, কিন্তু ভেতরে অনেকগুলো ঘর, আর প্রতিটি ঘরকে একটি নম্বর দিয়ে ধরা যায়। তাহলে “৭ নম্বর ঘর” লেখা যায়, আর “৭”-এর জায়গায় একটি ভ্যারিয়েবল বসিয়ে লুপে সব ঘর ঘোরা যায়। এই জিনিসটির নাম array.

ঢাকা মেট্রো রেলের একটি ট্রেনের কথা ভাবুন। ট্রেনে নির্দিষ্ট সংখ্যক কোচ, সবগুলো একই মাপের, একটির পর একটি জোড়া লাগানো, আর প্রতিটি কোচে নম্বর লেখা। স্টেশনে দাঁড়িয়ে আপনি বলতে পারেন “৩ নম্বর কোচে উঠব” — পুরো ট্রেন খুঁজতে হয় না, কারণ কোচগুলো সমান মাপের আর পাশাপাশি, তাই প্ল্যাটফর্মে কোন জায়গায় ৩ নম্বর থামবে তা আগেই হিসাব করা যায়। চলতি অবস্থায় ট্রেনে নতুন কোচ জোড়া যায় না। Array ঠিক এমন: নির্দিষ্ট সংখ্যক ঘর, সব একই টাইপের, পাশাপাশি, নম্বর দিয়ে সরাসরি পৌঁছানো যায়, আর তৈরির পর আকার স্থির।

int[] marks = {78, 85, 91, 64, 85};

Five int boxes side by side, numbered from 0

  1. marks[0]78প্রথম ঘর
  2. marks[1]85৪ byte পরে
  3. marks[2]91৮ byte পরে
  4. marks[3]64১২ byte পরে
  5. marks[4]85শেষ ঘর = length − 1
ছবি ১৩.১ — পাঁচটি ঘর, index ০ থেকে ৪। প্রতিটি int ৪ byte, তাই যেকোনো ঘরের অবস্থান একটি গুণ আর একটি যোগে বের হয়।

প্রতিটি ঘরের নম্বরকে বলে index. Java-তে index শুরু হয় ০ থেকে, তাই পাঁচ ঘরের array-এর শেষ index ৪। কেন ০? কারণ index আসলে “শুরু থেকে কত ঘর দূরে”। প্রথম ঘর শুরু থেকে ০ ঘর দূরে। ঘরগুলো মেমরিতে পাশাপাশি থাকে (contiguous memory), তাই JVM যেকোনো ঘরের ঠিকানা বের করে একটি সরল সূত্রে: শুরুর ঠিকানা + index × ঘরের মাপ। ঘর ০ হোক বা ঘর ৯,৯৯৯ — সময় লাগে একই। এই কারণেই array এত দ্রুত।

মূল ধারণা

Java-র array তিনটি জিনিস জানে, যা C-র array জানে না। এক, এটি একটি পূর্ণ object — new দিয়ে heap-এ তৈরি হয় (পর্ব ০৫-এর stack-heap ছবি মনে করুন)। দুই, সে নিজের দৈর্ঘ্য নিজের ভেতরে রাখে — marks.length. তিন, প্রতিটি ঘরে পড়া বা লেখার আগে JVM যাচাই করে index সঠিক সীমায় আছে কি না। ভুল index দিলে প্রোগ্রাম অন্য কারো মেমরিতে হাত দেয় না; সঙ্গে সঙ্গে একটি স্পষ্ট exception ছোড়ে।

২ · ঘোষণা, তৈরি আর শুরুর মান

Java-তে array নিয়ে কাজের দুটি আলাদা ধাপ আছে, আর এদের আলাদা করে বোঝা জরুরি: ভ্যারিয়েবল ঘোষণা আর array তৈরি।

int[] marks;               // ১. শুধু ভ্যারিয়েবল — এখনো কোনো array নেই
marks = new int[5];        // ২. heap-এ ৫ ঘরের array তৈরি, সব ঘরে 0

double[] temps = new double[7];         // ঘোষণা + তৈরি, এক লাইনে; সব ঘরে 0.0
int[] scores = {78, 85, 91};            // ঘোষণা + তৈরি + মান, এক লাইনে
String[] days = new String[]{"শনি", "রবি", "সোম"};  // মান দিয়ে, যেকোনো জায়গায়

প্রথম লাইনে int[] মানে “int-এর array”, আর marks হলো একটি ভ্যারিয়েবল যা এমন একটি array-এর reference রাখতে পারবে। এখনো কোনো ঘর তৈরি হয়নি। দ্বিতীয় লাইনে new int[5] heap-এ পাঁচটি int-এর জায়গা নেয়, আর তার reference যায় marks-এ। তৃতীয় লাইন দুটি ধাপ এক লাইনে সারে — ঢাকার ৭ দিনের তাপমাত্রার জন্য ৭টি double ঘর। চতুর্থ লাইনের {78, 85, 91} লেখার ধরনকে বলে array initializer — compiler নিজেই ঘর গুনে ৩ ঘরের array বানায়। এই সংক্ষিপ্ত রূপ শুধু ঘোষণার লাইনেই চলে; পরে নতুন মান দিতে চাইলে পঞ্চম লাইনের মতো new String[]{…} লিখতে হয়।

C থেকে আসা একটি লেখার ধরনও Java মেনে নেয়: int marks[] = new int[5];. কিন্তু Java-র নিজস্ব রীতি int[] marks — কারণ এতে টাইপ (int[]) আর নাম (marks) পরিষ্কার আলাদা থাকে। প্রচলিত style guide-গুলো (যেমন Google Java Style Guide) আর প্রায় সব প্রজেক্ট এই রীতি মানে।

Default value — নতুন array কখনো আবর্জনা দেখায় না

new int[5] লিখে কোনো মান না দিলে ঘরগুলোতে কী থাকে? C-তে উত্তর হতো “যা খুশি” — আগের কোনো প্রোগ্রামের ফেলে যাওয়া আবর্জনা। Java-তে প্রতিটি নতুন array-এর প্রতিটি ঘর নিশ্চিতভাবে একটি default value পায়:

ঘরের টাইপdefault valueউদাহরণ
int, long, short, byte0new int[3] → [0, 0, 0]
double, float0.0৭ দিনের তাপমাত্রার খালি খাতা
booleanfalseহাজিরা খাতা — শুরুতে সবাই অনুপস্থিত
charঅক্ষর নম্বর ০ (দেখা যায় না)ফাঁকা দেখায়, স্পেস নয়
যেকোনো reference (String, নিজের class)nullnew String[2] → [null, null]

শেষ সারিটি খেয়াল করুন: new String[2] দুটি String তৈরি করে না। এটি তৈরি করে দুটি খালি reference-ঘর, দুটিই null. কোনো ঘরে মান না বসিয়ে names[0].length() লিখলে আসবে NullPointerException — নতুনদের খুব সাধারণ ভুল।

length — field, method নয়

প্রতিটি array নিজের ঘরের সংখ্যা জানে: marks.length. এর পরে বন্ধনী নেই — এটি একটি final field-এর মতো আচরণ করে, যার মান তৈরির সময়ই স্থির হয়ে যায়। তুলনা করুন: String-এর দৈর্ঘ্য name.length() (method, বন্ধনীসহ), আর পর্ব ৩০-এর ArrayList-এর আকার list.size(). তিনটি জিনিস, তিন রকম লেখা — MCQ-তে এটি একটি পরিচিত ফাঁদ, আর কেন এমন হলো তা ভাবনার প্রশ্ন ৩-এ আছে। marks.length = 10; লিখলে compile error — দৈর্ঘ্য বদলানো যায় না।

৩ · Index আর সীমা — যেখানে Java পাহারা দেয়

কোনো ঘর পড়তে বা লিখতে লিখুন array-এর নাম, তারপর বর্গবন্ধনীতে index: marks[2] পড়ে তৃতীয় ঘর, marks[2] = 95; সেখানে নতুন মান লেখে। শেষ ঘর সবসময় marks[marks.length - 1]. Index হিসেবে যেকোনো int প্রকাশ চলে — marks[i], marks[i + 1], marks[n / 2].

এবার সেই প্রশ্ন, যা C আর Java-কে আলাদা করে দেয়: পাঁচ ঘরের array-এ marks[5] লিখলে কী হয়? C-তে কোনো যাচাই হয় না — প্রোগ্রাম চুপচাপ array-এর ঠিক পরের মেমরি পড়ে বা লিখে ফেলে, যেখানে হয়তো অন্য কোনো ভ্যারিয়েবল থাকে। ফল হতে পারে ভুল উত্তর, হঠাৎ ক্র্যাশ, অথবা নিরাপত্তা-ছিদ্র। Java-তে এমন কোড compile হয় (compiler সাধারণত জানে না চলার সময় index কত হবে), কিন্তু চলার সময় JVM প্রতিটি array-ঘরে ঢোকার আগে যাচাই করে 0 ≤ index < length কি না। না হলে সে ছোড়ে ArrayIndexOutOfBoundsException:

class Main {
    public static void main(String[] args) {
        int[] marks = {78, 85, 91, 64, 85};
        System.out.println(marks[5]);   // ঘর আছে 0 থেকে 4
    }
}

Java 21-এ চালালে terminal-এ আসে:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
	at Main.main(Main.java:4)

এই বার্তাটি পড়তে শিখুন, কারণ জীবনে বহুবার দেখবেন। Index 5 — আপনি কোন index চেয়েছিলেন। for length 5 — array-এ আসলে কয়টি ঘর। অর্থাৎ বৈধ index ছিল ০ থেকে ৪। পরের লাইন at Main.main(Main.java:4) হলো stack trace — ভুলটি কোন ফাইলের কোন লাইনে। এই স্পষ্ট বার্তা Java 11 থেকে; পুরোনো Java শুধু ArrayIndexOutOfBoundsException: 5 লিখত। Index ঋণাত্মক হলেও একই exception: Index -1 out of bounds for length 5.

সবচেয়ে পরিচিত কারণ হলো off-by-one ভুল — লুপের শর্তে i <= marks.length লেখা, যেখানে লেখা উচিত i < marks.length. শেষ পাকে i হয় ৫, আর exception. সমাধান সহজ: যেখানে সম্ভব for-each ব্যবহার করুন, যেখানে index লাগে সেখানে শর্তে < আর length লিখুন — কখনো হাতে লেখা সংখ্যা নয়।

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

C-তে array একটি নাম দেওয়া মেমরি-খণ্ড মাত্র: দৈর্ঘ্য জানে না, function-এ পাঠালে pointer হয়ে যায়, আর সীমা যাচাই নেই (C-র array পর্ব)। C++-এর std::vector-এ v[i] যাচাই করে না, v.at(i) করে (C++ vector পর্ব); Java-তে প্রতিটি index-ই যাচাই হয়। Python-এর list Java-র array নয়, বরং ArrayList-এর কাছাকাছি — আকার বাড়ে, যেকোনো টাইপ রাখে, আর a[-1] দিয়ে শেষ ঘর পাওয়া যায় (Python list পর্ব); Java-তে ঋণাত্মক index মানেই exception.

৪ · লুপে array — for, for-each আর চারটি পরিচিত কাজ

Array আর লুপ একসাথে জন্মানো জুটি। পর্ব ১১-এ দুই রকম লুপ দেখেছেন; array-তে দুটিরই নিজস্ব জায়গা আছে:

// index-ওয়ালা for: কোন ঘরে আছি জানা দরকার, বা ঘর বদলাতে হবে
for (int i = 0; i < marks.length; i++) {
    marks[i] = marks[i] + 5;          // সবাইকে ৫ নম্বর গ্রেস
}

// for-each: শুধু প্রতিটি মান পড়ব, index লাগবে না
for (int m : marks) {
    System.out.println(m);
}

for-each লুপ (for (int m : marks)) পড়ুন “marks-এর প্রতিটি m-এর জন্য”। এতে index নেই, তাই off-by-one ভুলের সুযোগও নেই। কিন্তু দুটি সীমা মনে রাখুন। এক, m হলো ঘরের মানের একটি কপি — m = 0; লিখলে array বদলায় না। দুই, কোন ঘরে আছেন তা জানা যায় না। তাই ঘর বদলাতে বা পাশের ঘরের সাথে তুলনা করতে index-ওয়ালা for লাগে।

এবার সবচেয়ে পরিচিত কাজটি ধাপে ধাপে দেখি — ঢাকার পাঁচ দিনের সর্বোচ্চ তাপমাত্রা থেকে সবচেয়ে গরম দিন খোঁজা। মূল কৌশল হলো একটি “এখন পর্যন্ত সেরা” ভ্যারিয়েবল, যা প্রতিটি ঘরের সাথে তুলনা করে দরকার হলে বদলায়:

1int[] t = {33, 35, 31, 36, 34};2int max = t[0];3for (int x : t) {4    if (x > max) max = x;5}6System.out.println("max = " + max);
ভ্যারিয়েবলের অবস্থা (সবুজ লাইনটি এইমাত্র চলেছে)
লুকানো index
x
max
  1. শুরু: max = t[0]. প্রথম ঘরকেই আপাতত “সেরা” ধরা হলো। কেন max = 0 নয়? কারণ সব মান ঋণাত্মক হলে (যেমন শীতের দেশের তাপমাত্রা −৫, −২, −৮) ০ কখনো হারবে না, আর উত্তর আসবে ভুল ০। প্রথম ঘর দিয়ে শুরু করা সবসময় নিরাপদ — শর্ত একটাই, array খালি হলে চলবে না।
  2. প্রথম পাক: x = 33. for-each ভেতরে ভেতরে একটি index রাখে (০), আর সেই ঘরের মান কপি করে x-এ দেয়। ৩৩ কি ৩৩-এর চেয়ে বড়? না। max যেমন ছিল তেমনই।
  3. দ্বিতীয় পাক: x = 35. ৩৫ > ৩৩, তাই max হলো ৩৫। “এখন পর্যন্ত দেখা সবচেয়ে বড়” — এই অর্থ max সবসময় ধরে রাখে; একে বলে লুপের invariant.
  4. তৃতীয় পাক: x = 31. ৩১ > ৩৫ নয়। কিছু বদলায় না। লক্ষ করুন, প্রতি পাকে মাত্র একটি তুলনা।
  5. চতুর্থ পাক: x = 36. ৩৬ > ৩৫, max হলো ৩৬। যদি জানতে চাইতেন কোন দিন সবচেয়ে গরম, তবে মানের সাথে index-ও রাখতে হতো — আর তখন for-each নয়, index-ওয়ালা for লাগত।
  6. পঞ্চম পাক: x = 34. ৩৪ > ৩৬ নয়। এটাই শেষ ঘর; for-each নিজেই থামবে। আপনাকে length লিখতে হয়নি, তাই সীমা ভুল করারও সুযোগ ছিল না।
  7. লুপ শেষ: ছাপা হলো max = 36. পাঁচটি ঘর, পাঁচটি তুলনা। n ঘরের array-এ এই কাজে লাগে nটি ধাপ — array যত বড়, সময় তত সমানুপাতিক হারে বাড়ে।

আরও তিনটি কাজ, যা বারবার লাগবে

একটি দোকানের ১২ মাসের বিক্রি একটি array-তে আছে ধরুন। নিচের তিনটি ছাঁচ প্রায় প্রতিটি প্রোগ্রামে কোনো না কোনো রূপে আসে:

// ১. linear search: ৫০০০ টাকা বিক্রি হয়েছে কোন মাসে?
int found = -1;
for (int i = 0; i < sales.length; i++) {
    if (sales[i] == 5000) { found = i; break; }
}

// ২. গোনা: কত মাসে বিক্রি ৪০০০-এর বেশি?
int count = 0;
for (int s : sales) if (s > 4000) count++;

// ৩. উল্টানো (নতুন array ছাড়া, জায়গায় দাঁড়িয়ে)
for (int i = 0, j = sales.length - 1; i < j; i++, j--) {
    int tmp = sales[i]; sales[i] = sales[j]; sales[j] = tmp;
}

Linear search শুরু থেকে একটি একটি করে দেখে; না পেলে -1 ফেরত দেওয়া একটি প্রচলিত রীতি, কারণ -১ কখনো বৈধ index নয়। উল্টানোর কৌশলে দুই প্রান্ত থেকে দুটি index মাঝের দিকে আসে, প্রতিবার দুটি ঘর অদলবদল করে, আর দেখা হলেই থামে — অর্ধেক পাকেই কাজ শেষ। শর্ত i < j খেয়াল করুন: i <= j দিলেও ক্ষতি নেই, কিন্তু i < length দিলে দ্বিতীয়ার্ধে আবার অদলবদল হয়ে array আগের রূপে ফিরে যায় — একটি ক্লাসিক ভুল।

৫ · Array একটি reference — শেয়ার, কপি আর method-এ পাঠানো

পর্ব ০৫-এ দেখেছেন, object-এর ভ্যারিয়েবলে object নিজে থাকে না, থাকে তার reference. Array-ও object, তাই একই নিয়ম। এর একটি ফল প্রথমবার প্রায় সবাইকে চমকে দেয়। নিচের পাঁচ লাইন ধাপে ধাপে দেখুন:

1int[] a = {10, 20, 30};2int[] b = a;3b[0] = 99;4System.out.println(a[0]);5b = new int[]{1, 2, 3};
মেমরির অবস্থা (সবুজ লাইনটি এইমাত্র চলেছে)
stack: a
stack: b
heap: array #1
heap: array #2
  1. int[] a = {10, 20, 30}; — heap-এ তিন ঘরের একটি array তৈরি হলো (ধরুন নাম “array #1”), আর stack-এর a ঘরে রাখা হলো তার reference. a নিজে array নয়; array কোথায় আছে, তার খোঁজ।
  2. int[] b = a; — কোনো নতুন array তৈরি হয়নি। a-র ভেতরের reference কপি হলো b-তে। এখন একই array-এর দুটি নাম। একে বলে aliasing — একই জিনিসের একাধিক ডাকনাম।
  3. b[0] = 99; — b যে array দেখায়, তার ঘর ০-এ ৯৯ লেখা হলো। সেটি তো array #1 — a-ও ঠিক সেটিই দেখায়।
  4. System.out.println(a[0]); ছাপে 99. আপনি a-তে কখনো হাত দেননি, তবু তার প্রথম ঘর বদলে গেছে। AI-এর লেখা কোডে বা দলের কোডে এই বাগ খুঁজে পেতে ঘণ্টার পর ঘণ্টা যায়, যদি এই ছবিটি মাথায় না থাকে।
  5. b = new int[]{1, 2, 3}; — এবার b-কে সম্পূর্ণ নতুন একটি array দেওয়া হলো (array #2). এতে array #1 বা a-র কিছুই বদলায় না; শুধু b-এর খোঁজ এখন অন্য জায়গায়। পার্থক্যটি মনে রাখুন: b[0] = … array-এর ভেতরটা বদলায়, b = … ভ্যারিয়েবলটা বদলায়।

সত্যিকারের কপি — কয়েকটি উপায়

আলাদা একটি array চাইলে, যেটা বদলালে আসলটি অক্ষত থাকে, কয়েকটি প্রচলিত উপায় আছে:

উপায়কী করেকখন
a.clone()একই দৈর্ঘ্যের নতুন array, সব মান কপিহুবহু পুরো কপি
Arrays.copyOf(a, n)নতুন array, দৈর্ঘ্য n; ছোট হলে কেটে যায়, বড় হলে বাকি ঘরে default valueআকার বাড়ানো বা কমানো
Arrays.copyOfRange(a, from, to)from থেকে to-এর আগ পর্যন্ত অংশএকটি টুকরো দরকার
System.arraycopy(src, i, dest, j, n)আগে থেকে থাকা array-এ nটি ঘর কপিবড় ডেটা, দ্রুততম কাজ

একটি মজার তথ্য: “array-এর আকার বাড়ানো” আসলে Java-তে অসম্ভব। marks = Arrays.copyOf(marks, 10); লিখলে পুরোনো array বড় হয় না — নতুন ১০ ঘরের array তৈরি হয়, পুরোনো মানগুলো তাতে কপি হয়, আর ভ্যারিয়েবল নতুনটিকে দেখাতে শুরু করে। পুরোনোটি garbage collector-এর জন্য পড়ে থাকে। পর্ব ৩০-এর ArrayList ভেতরে ঠিক এই কাজটিই করে, আপনার হয়ে।

এই কপিগুলো shallow. String[] বা int[][]-এর মতো reference-ভরা array-তে clone() শুধু reference-গুলো কপি করে, ভেতরের object বা সারিগুলো নয়। ফলে 2D array clone করার পর copy[0][0] = 5; লিখলে আসলটিও বদলায়, কারণ দুটির সারি একই। একে বলে shallow copy; পুরো স্বাধীন কপির জন্য প্রতিটি সারি আলাদা করে কপি করতে হয়।

Method-এ array পাঠালে

পর্ব ১২-এ দেখেছেন, Java সবসময় pass-by-value — method পায় argument-এর একটি কপি। Array-এর ক্ষেত্রে কপি হয় reference-টি, array নয়। তাই method array-এর ভেতরের ঘর বদলালে সেই বদল ডাকা জায়গাতেও দেখা যায়; কিন্তু method যদি নিজের parameter-কে নতুন array দেয়, বাইরের ভ্যারিয়েবল তা টেরও পায় না — ঠিক উপরের stepper-এর ধাপ ৩ আর ধাপ ৫-এর পার্থক্য।

static void addGrace(int[] m) { for (int i = 0; i < m.length; i++) m[i] += 5; }  // বাইরে দেখা যাবে
static void reset(int[] m)    { m = new int[m.length]; }                          // বাইরে কোনো প্রভাব নেই

এটাও খেয়াল করুন: C-তে function-এ array পাঠালে দৈর্ঘ্য হারিয়ে যায়, তাই আলাদা n পাঠাতে হয়। Java-তে m.length সবসময় সঙ্গে যায়। আর পর্ব ১২-এর varargs (int... nums) আসলে ছদ্মবেশী array — method-এর ভেতরে nums একটি সাধারণ int[].

এবার এই অংশের সবকিছু একসাথে চালিয়ে দেখুন — চলমান max আর গড়, default value, aliasing আর সঠিক কপি, আর তুলনার সঠিক উপায়:

import java.util.Arrays;

class Main {
    public static void main(String[] args) {
        int[] marks = {78, 85, 91, 64, 85};
        int sum = 0, max = marks[0];
        for (int m : marks) {
            sum += m;
            if (m > max) max = m;
        }
        System.out.println("marks  = " + Arrays.toString(marks));
        System.out.println("length = " + marks.length + ", max = " + max);
        System.out.printf("avg    = %.1f%n", (double) sum / marks.length);

        int[] alias = marks;                              // একই array
        int[] copy = Arrays.copyOf(marks, marks.length);  // নতুন array
        alias[0] = 99;
        System.out.println("marks[0] = " + marks[0] + ", copy[0] = " + copy[0]);
        System.out.println("copy == marks : " + (copy == marks));
        System.out.println("Arrays.equals : " + Arrays.equals(copy, new int[]{78, 85, 91, 64, 85}));

        System.out.println(Arrays.toString(new boolean[3]) + " " + Arrays.toString(new String[2]));
    }
}

আউটপুট সাত লাইন: marks = [78, 85, 91, 64, 85], length = 5, max = 91, avg = 80.6, marks[0] = 99, copy[0] = 78, copy == marks : false, Arrays.equals : true, আর [false, false, false] [null, null]. গড়ের লাইনে (double) না দিলে 403 / 5 integer ভাগ হয়ে 80 আসত (পর্ব ০৭)। alias দিয়ে বদলানোয় marks[0] হলো ৯৯, কিন্তু copy আলাদা array বলে অক্ষত। == জিজ্ঞেস করে “একই array কি?” — উত্তর না; Arrays.equals জিজ্ঞেস করে “ঘরে ঘরে মান কি এক?” — উত্তর হ্যাঁ। শেষ লাইনে default value নিজের চোখে দেখলেন। (নিজের প্রজেক্টে main class সাধারণত public class Main; এই পাতার রানারের জন্য public বাদ রাখা হয়েছে।)

৬ · java.util.Arrays — array-এর সহকারী

Array নিজে খুব সরল object — তার কোনো দরকারি method নেই (না আছে sort, না সুন্দর করে ছাপা)। সেই কাজগুলো রাখা আছে java.util.Arrays নামের একটি class-এ, যার সব method static. ফাইলের শুরুতে import java.util.Arrays; লিখে ব্যবহার করুন।

প্রথম ফাঁদ: array সরাসরি ছাপা

System.out.println(marks); লিখলে কী ছাপবে বলে মনে হয়? [78, 85, 91]? না। আসবে এমন কিছু:

[I@1b6d3586

এটা কোনো ভুল নয়, কিন্তু কাজেরও নয়। Array-এর নিজস্ব toString নেই, তাই Object-এর ডিফল্টটি চলে: টাইপের ভেতরের নাম, @, আর একটি hexadecimal সংখ্যা। [ মানে “array”, I মানে int (double[] হলে [D, String[] হলে [Ljava.lang.String;). @-এর পরের সংখ্যাটি object-এর identity hash code — প্রতিবার চালালে বা প্রতিটি কম্পিউটারে আলাদা হতে পারে। এর ওপর কখনো নির্ভর করবেন না। সবসময় লিখুন Arrays.toString(marks); 2D array-এর জন্য Arrays.deepToString(table).

একটি ব্যতিক্রম আছে, যা বিভ্রান্ত করে: char[] সরাসরি println-এ দিলে অক্ষরগুলো জোড়া লেগে লেখা হিসেবে ছাপে, কারণ println-এর একটি আলাদা সংস্করণ শুধু char[] নেয়। কিন্তু "নাম: " + chars লিখলে আবার সেই [C@…. নিয়ম সহজ রাখুন: array ছাপতে সবসময় Arrays.toString.

প্রয়োজনীয় method-গুলো এক নজরে

methodকাজসতর্কতা
Arrays.toString(a)[78, 85, 91] রূপে লেখা2D হলে deepToString
Arrays.sort(a)ছোট থেকে বড় সাজায়, জায়গায় দাঁড়িয়েআসল array বদলে যায়; আগের ক্রম লাগলে আগে কপি
Arrays.binarySearch(a, key)সাজানো array-এ দ্রুত খোঁজে, index দেয়না সাজানো array-এ ফল অর্থহীন; না পেলে ঋণাত্মক
Arrays.fill(a, v)সব ঘরে একই মান2D-তে সারি ধরে ধরে
Arrays.equals(a, b)দৈর্ঘ্য আর ঘরে ঘরে মান এক কি নাa == b শুধু একই object কি না দেখে; 2D-তে deepEquals

Binary search — প্রতিবার অর্ধেক বাদ

Linear search ১০ লাখ ঘরে সবচেয়ে খারাপ অবস্থায় ১০ লাখ ঘর দেখে। কিন্তু array যদি সাজানো থাকে, তাহলে অনেক চতুর কৌশল চলে — অভিধানে শব্দ খোঁজার মতো। মাঝখানের ঘর দেখুন; খোঁজা মান বড় হলে বাঁ দিকের অর্ধেক পুরো বাদ, ছোট হলে ডান দিক বাদ। প্রতি ধাপে বাকি জায়গা অর্ধেক হয়, তাই ১০ লাখ ঘরেও ২০টির মতো ধাপ যথেষ্ট। একে বলে binary search. দশজনের সাজানো নম্বর থেকে ৯১ খুঁজে দেখি:

1int lo = 0, hi = a.length - 1;2int mid = (lo + hi) >>> 1;3if (a[mid] < key) lo = mid + 1;4else if (a[mid] > key) hi = mid - 1;5else return mid;   // পাওয়া গেছে
a = [45, 52, 60, 64, 70, 78, 85, 88, 91, 97], key = 91
lo
hi
mid → a[mid]
বাকি জায়গা
  1. শুরু। lo আর hi হলো সম্ভাব্য জায়গার দুই প্রান্ত — শুরুতে পুরো array (০ থেকে ৯)। শর্ত একটাই: array ছোট থেকে বড় সাজানো থাকতে হবে, নইলে “বাঁ দিকে সব ছোট” যুক্তিটাই ভেঙে পড়ে।
  2. প্রথম ধাপ: mid = 4, a[4] = 70. ৭০ < ৯১, আর array সাজানো, তাই index ৪ ও তার বাঁয়ের সব মান ৯১-এর চেয়ে ছোট — একটি তুলনায় পাঁচটি ঘর বাদ। lo হলো ৫। >>> 1 হলো ২ দিয়ে ভাগের নিরাপদ রূপ — কেন, তা নিচে বলা আছে।
  3. দ্বিতীয় ধাপ: mid = 7, a[7] = 88. ৮৮ < ৯১, আবার বাঁ দিক বাদ। বাকি মাত্র দুটি ঘর: index ৮ আর ৯।
  4. তৃতীয় ধাপ: mid = 8, a[8] = 91 — পাওয়া গেছে। ফেরত index ৮। দশ ঘরে তিনটি তুলনা; linear search লাগত নয়টি। খোঁজা মান না থাকলে Arrays.binarySearch একটি ঋণাত্মক সংখ্যা দেয়: -(যেখানে বসানো উচিত ছিল) - 1. যেমন এই array-এ ৪০ খুঁজলে আসবে -১, কারণ ৪০ বসত index ০-তে।

Binary search-এর ধারণা সহজ, কিন্তু নির্ভুলভাবে লেখা কুখ্যাতভাবে কঠিন। ২০০৬ সালে Google-এর Joshua Bloch একটি ব্লগ লেখায় জানান, Java-র নিজের Arrays.binarySearch-এ প্রায় নয় বছর ধরে একটি বাগ ছিল: মাঝের ঘর বের করা হতো (low + high) / 2 দিয়ে। Array-এ শত কোটির বেশি ঘর থাকলে low + high যোগফল int-এর সীমা (প্রায় ২১৪ কোটি) ছাড়িয়ে ঋণাত্মক হয়ে যেত, আর আসত ArrayIndexOutOfBoundsException. সমাধান ছিল (low + high) >>> 1 — unsigned shift ঋণাত্মক হয়ে যাওয়া বিটগুলোকে আবার সঠিক ধনাত্মক ফলে ফিরিয়ে আনে। শিক্ষা: অনেক বছর ধরে চলা “প্রমাণিত” কোডও বড় ডেটায় ভাঙতে পারে।

৭ · 2D array — array-এর array

৫ জন শিক্ষার্থীর ৩টি বিষয়ের নম্বর — এটি একটি টেবিল: সারি মানে শিক্ষার্থী, কলাম মানে বিষয়। Java-তে লিখবেন int[][] marks = new int[5][3]; আর তৃতীয় শিক্ষার্থীর দ্বিতীয় বিষয় পড়বেন marks[2][1] দিয়ে — আগে সারি, তারপর কলাম।

কিন্তু ভেতরের ছবিটি C-র মতো একটি বড় আয়তক্ষেত্র নয়। Java-তে আসলে আলাদা কোনো “2D array” নেই। int[][] মানে “int[]-এর array” — একটি বাইরের array, যার প্রতিটি ঘরে একটি সারির reference, আর প্রতিটি সারি heap-এ আলাদা একটি int[] object. এই ছবি থেকেই একটি সুবিধা আসে: সারিগুলোর দৈর্ঘ্য আলাদা হতে পারে। একে বলে jagged array. ধরুন একটি বাসের সিট-প্ল্যান — সামনের সারিগুলোতে ৪টি করে সিট, পেছনের শেষ সারিতে ৫টি:

1int[][] seats = new int[3][];2seats[0] = new int[4];3seats[1] = new int[4];4seats[2] = new int[5];5seats[2][4] = 1;   // শেষ সিট বুকড
heap-এর অবস্থা (সবুজ লাইনটি এইমাত্র চলেছে)
seats (বাইরের array)
সারি০
সারি১
সারি২
heap-এ মোট array object
  1. new int[3][] — দ্বিতীয় বন্ধনী খালি। তৈরি হলো শুধু বাইরের array, তিনটি ঘরের, আর প্রতিটি ঘরে null — কারণ ঘরগুলোর টাইপ int[], একটি reference টাইপ। এখন seats[0][0] লিখলে NullPointerException, কারণ সারিই নেই।
  2. seats[0] = new int[4]; — heap-এ আলাদা একটি ৪ ঘরের int[] তৈরি হলো, সব ০, আর তার reference বসল বাইরের array-এর ঘর ০-এ।
  3. seats[1] = new int[4]; — আরেকটি স্বাধীন object. মেমরিতে সারি০-এর ঠিক পাশেই থাকবে, এমন কোনো নিশ্চয়তা নেই।
  4. seats[2] = new int[5]; — এই সারি লম্বা। বাইরের array-এর কিছু যায় আসে না; তার কাছে প্রতিটি ঘর শুধু একটি reference. তাই লুপে সবসময় seats[r].length ব্যবহার করুন, কখনো ধরে নেবেন না যে সব সারি সমান। new int[3][4] লিখলে Java একসাথে চারটি object বানায় — বাইরের একটি আর সমান মাপের তিনটি সারি।
  5. seats[2][4] = 1; — পড়ুন বাঁ থেকে ডানে: seats[2] দিয়ে সারি২-এর reference পাওয়া গেল, তারপর সেই array-এর [4]. দুবার সীমা যাচাই হয় — বাইরের array-এ একবার, সারিতে একবার। সারি০-তে [4] লিখলে exception আসত, সারি২-তে আসে না।

এবার ঢাকার এক সপ্তাহের তাপমাত্রা সাজিয়ে binary search, আর বাসের সিট-প্ল্যান চালিয়ে দেখুন:

import java.util.Arrays;

class Main {
    public static void main(String[] args) {
        // ঢাকার ৭ দিনের সর্বোচ্চ তাপমাত্রা (°C)
        int[] temps = {33, 35, 31, 36, 34, 32, 35};
        int[] sorted = temps.clone();          // আসল ক্রম রেখে দিলাম
        Arrays.sort(sorted);
        System.out.println("sorted = " + Arrays.toString(sorted));
        System.out.println("34 at index " + Arrays.binarySearch(sorted, 34));
        System.out.println("30 -> " + Arrays.binarySearch(sorted, 30));
        System.out.println("temps  = " + Arrays.toString(temps));

        // বাসের সিট: দুই সারিতে ৪টি, শেষ সারিতে ৫টি (jagged)
        char[][] seats = {new char[4], new char[4], new char[5]};
        for (char[] row : seats) Arrays.fill(row, '.');
        seats[0][1] = 'X';
        seats[2][4] = 'X';
        for (char[] row : seats) System.out.println(new String(row));

        int[][] table = {{78, 85, 91}, {64, 70}};
        System.out.println(Arrays.deepToString(table));
    }
}

আউটপুট: sorted = [31, 32, 33, 34, 35, 35, 36], 34 at index 3, 30 -> -1, temps = [33, 35, 31, 36, 34, 32, 35], তারপর সিট-প্ল্যানের তিন লাইন .X.., ...., ....X, আর শেষে [[78, 85, 91], [64, 70]]. ৩০ array-এ নেই; থাকলে বসত index ০-তে, তাই ফল -(0) - 1 = -1. clone() করেছিলাম বলে temps-এর আসল ক্রম অক্ষত — নইলে Arrays.sort সেটিকেই বদলে দিত, আর “কোন দিন সবচেয়ে গরম” প্রশ্নের উত্তর হারিয়ে যেত। সিট-প্ল্যানে প্রতিটি সারিকে Arrays.fill আলাদা করে ভরেছে, কারণ প্রতিটি সারি আলাদা array.

৮ · Array নাকি ArrayList — আর main-এর String[] args

Array-এর সবচেয়ে বড় সীমা তার স্থির আকার। একটি অনলাইন দোকানের অর্ডার, একটি চ্যাট অ্যাপের মেসেজ, ফাইল থেকে পড়া লাইন — এগুলো কয়টি হবে আগে জানা যায় না। এমন জায়গার জন্য Java-র Collections-এ আছে ArrayList, যা প্রয়োজন মতো বড় হয় (পর্ব ৩০-এ বিস্তারিত)। ভেতরে সে একটি array-ই রাখে; জায়গা ফুরালে বড় একটি নতুন array বানিয়ে সব কপি করে — উপরে Arrays.copyOf-এর যে কৌশল দেখলেন।

📦 array বেছে নিন যখন

আকার আগে থেকেই জানা আর স্থির (সপ্তাহের ৭ দিন, বছরের ১২ মাস, বোর্ডের ৮×৮ ঘর); int, double-এর মতো primitive বিপুল পরিমাণে রাখতে হবে; গতি ও মেমরি গুরুত্বপূর্ণ (ছবি, অডিও, গাণিতিক হিসাব, কনটেস্ট)।

স্থির, হালকা, দ্রুত

📋 ArrayList বেছে নিন যখন

কতগুলো উপাদান আসবে জানা নেই; মাঝখানে যোগ-বাদ হবে; contains, remove, সুন্দর toString-এর মতো সুবিধা চান; অন্য Collections API-র সাথে কাজ করবেন। আধুনিক ব্যবসায়িক কোডে এটাই সাধারণ পছন্দ।

নমনীয়, সুবিধাজনক

Primitive-এর ব্যাপারটি একটু খুলে বলি। int[]-এর প্রতিটি ঘরে সরাসরি একটি ৪ byte-এর সংখ্যা, পাশাপাশি সাজানো। কিন্তু ArrayList<Integer> primitive রাখতে পারে না; প্রতিটি সংখ্যা একটি আলাদা Integer object হয়ে heap-এ থাকে, আর list-এ থাকে তাদের reference. ফলে ১০ লাখ সংখ্যায় ArrayList কয়েক গুণ বেশি মেমরি নেয়, আর প্রতিটি মান পড়তে একটি বাড়তি লাফ লাগে। দৈনন্দিন কোডে এই পার্থক্য চোখে পড়ে না; বিপুল ডেটায় পড়ে।

প্রথম দিন থেকে দেখা array — String[] args

পর্ব ০৪ থেকে প্রতিটি প্রোগ্রামে লিখে আসছেন public static void main(String[] args). এখন বুঝতে পারছেন args কী: String-এর একটি array. Terminal-এ java Main Dhaka 42 চালালে JVM args-এ দেয় ["Dhaka", "42"] — দুটিই লেখা, ৪২-ও; সংখ্যা চাইলে Integer.parseInt(args[1]). কিছু না দিলে args null নয়, বরং ০ দৈর্ঘ্যের একটি খালি array. তাই args[0] পড়ার আগে args.length যাচাই করা ভালো অভ্যাস; নইলে সেই পরিচিত বার্তা: Index 0 out of bounds for length 0. এই কমান্ড-লাইন argument-ই (command-line argument) git, javac-এর মতো টুলগুলোকে নির্দেশ দেয়।

পরীক্ষা ও ইন্টারভিউতে
  • Array-এর দৈর্ঘ্য, String-এর দৈর্ঘ্য আর ArrayList-এর আকার কীভাবে পাবেন? — arr.length (field), str.length() (method), list.size() (method).
  • int[] a = new int[5];-এর পর a[5] পড়লে কী হয়? — compile হয়, কিন্তু চলার সময় ArrayIndexOutOfBoundsException; বৈধ index ০ থেকে ৪।
  • দুটি array-এর মান এক কি না কীভাবে দেখবেন? — Arrays.equals(a, b); a == b শুধু দেখে দুটি একই object কি না।
  • Java-তে array কি primitive না object? — object; heap-এ থাকে, Object-এর সব method আছে, আর ভ্যারিয়েবলে থাকে reference.
  • Array আর ArrayList-এর মূল পার্থক্য? — array স্থির আকারের ও primitive সরাসরি রাখে; ArrayList আকার বদলায় কিন্তু শুধু object (wrapper) রাখে।

৯ · AI-এর যুগে — AI-এর লেখা array কোড যাচাই

Array নিয়ে AI-এর কোড সাধারণত compile হয়, আর ছোট পরীক্ষায় ঠিকঠাক চলে। ভুলগুলো লুকিয়ে থাকে সীমায়, reference-এ আর ছাপার লাইনে — ঠিক যে জায়গাগুলো এই পর্বে দেখলেন। নিচের টেবিলটি AI-এর উত্তরে বারবার দেখা ভুলের তালিকা:

AI যা লেখেসমস্যাসঠিক রূপ
System.out.println(arr);[I@… ছাপে, মান নয়Arrays.toString(arr)
if (a == b) দিয়ে array তুলনাশুধু একই object কি না দেখেArrays.equals(a, b)
for (i = 0; i <= n; i++)শেষ পাকে সীমার বাইরেi < arr.length বা for-each
int max = 0;সব মান ঋণাত্মক হলে ভুল উত্তরint max = arr[0]; (খালি কিনা আগে যাচাই)
int[] copy = original; “কপি” বলেশুধু reference কপি, aliasingoriginal.clone() বা Arrays.copyOf
না সাজিয়ে binarySearchফল অনির্ভরযোগ্যআগে Arrays.sort, বা linear search
আকার অজানা, তবু new int[1000]অপচয় বা জায়গা ফুরানোArrayList (পর্ব ৩০)

উল্টো ভুলটিও দেখা যায়: ১০ লাখ int নিয়ে গাণিতিক হিসাবের কোডে AI ArrayList<Integer> বসিয়ে দেয়, যেখানে সাধারণ int[] অনেক দ্রুত আর হালকা হতো। AI-কে প্রসঙ্গ না দিলে সে “সাধারণত যা লেখা হয়” সেটাই লেখে, আপনার সমস্যার জন্য যা সেরা সেটা নয়।

Array-ভিত্তিক কোড চাইলে AI-কে সীমা, কপি আর ছাপার নিয়ম আগেই বেঁধে দিন:

আমি Java 21 ব্যবহার করি। একটি ক্লাসের ৪০ জন শিক্ষার্থীর ৩টি বিষয়ের নম্বর একটি int[][] array-তে আছে। প্রতিটি শিক্ষার্থীর মোট নম্বর আর সর্বোচ্চ নম্বর পাওয়া শিক্ষার্থীর index বের করার একটি method লিখে দাও। শর্ত: ১. array ছাপতে শুধু Arrays.toString বা Arrays.deepToString ব্যবহার করবে, সরাসরি println নয়। ২. লুপের শর্তে হাতে লেখা সংখ্যা নয়, সবসময় .length ব্যবহার করবে; সারির দৈর্ঘ্য আলাদা হতে পারে ধরে নেবে। ৩. max শুরু করবে প্রথম উপাদান দিয়ে, 0 দিয়ে নয়; খালি array বা null এলে কী হবে, তা স্পষ্ট করে সামলাবে। ৪. মূল array বদলাবে না; কপি লাগলে কেন লাগছে লিখবে। ৫. এখানে array নাকি ArrayList ভালো, এক লাইনে কারণসহ বলবে। ৬. খালি array, এক জন শিক্ষার্থী আর সমান সর্বোচ্চ নম্বর — এই তিন ক্ষেত্রে আউটপুট কী হবে, তা দেখাবে।

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

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

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

প্র ০১Java প্রতিটি array-ঘরে সীমা যাচাই করে, C করে না। এই নিরাপত্তার দাম কত — আর JIT কীভাবে সেই দামের বেশিরভাগ মাফ করিয়ে দেয়?

C-র নকশা হয়েছিল ১৯৭০-এর দশকের ধীর, ছোট মেমরির কম্পিউটারের জন্য, যেখানে প্রতিটি নির্দেশ দামি। প্রতিটি array-ঘরে “index কি সীমার ভেতরে?” যাচাই মানে বাড়তি তুলনা আর শাখা — তাই C সেই দায়িত্ব প্রোগ্রামারের হাতে ছেড়ে দেয়। কিন্তু মানুষ ভুল করে, আর এই একটি সিদ্ধান্তের দাম ইতিহাস বহুবার দিয়েছে। ১৯৮৮ সালের Morris worm ইন্টারনেটের হাজার হাজার কম্পিউটারে ছড়িয়েছিল আংশিকভাবে fingerd প্রোগ্রামের একটি buffer overflow কাজে লাগিয়ে। ২০১৪ সালের Heartbleed ছিল OpenSSL-এর একটি over-read — যাচাইহীন দৈর্ঘ্যের কারণে সার্ভার নিজের মেমরি থেকে গোপন তথ্য, এমনকি private key পর্যন্ত, ফাঁস করে দিত। দুটিই ঘটেছে “সীমা যাচাই নেই” থেকে।

Java-র নকশাকারেরা ১৯৯০-এর দশকে নেটওয়ার্ক থেকে নামানো অপরিচিত কোড নিরাপদে চালানোর কথা ভাবছিলেন। সেখানে মেমরি-নিরাপত্তা ঐচ্ছিক হতে পারে না। তাই JVM-এর specification-এ লেখা হলো: প্রতিটি array access (iaload, iastore ইত্যাদি bytecode) সীমা যাচাই করবে, ব্যর্থ হলে exception — কোনো ব্যতিক্রম নেই।

তাহলে কি Java-র প্রতিটি লুপ ধীর? বাস্তবে খুব কম। HotSpot-এর JIT compiler (পর্ব ০৫) কোড বিশ্লেষণ করে প্রমাণ করতে পারে কখন যাচাই অপ্রয়োজনীয়। for (int i = 0; i < a.length; i++) লুপে i শূন্য থেকে শুরু, প্রতি পাকে এক করে বাড়ে, আর a.length-এর আগে থামে — তাহলে a[i] কখনো সীমার বাইরে যেতে পারে না, আর JIT ভেতরের যাচাই পুরো মুছে দেয়। জটিল ক্ষেত্রে সে যাচাইটিকে লুপের বাইরে একবারে নিয়ে আসে (একে বলে range check elimination ও loop predication). ফলে সরল লুপে Java-র array গতিতে C-র কাছাকাছি চলে।

আরেকটি বিষয়: সীমা যাচাই সব বাগ দূর করে না। ভুল index এখনো ভুল — শুধু পার্থক্য হলো, প্রোগ্রাম চুপচাপ ভুল মেমরি নষ্ট না করে সঙ্গে সঙ্গে ঠিক লাইনের নাম বলে থেমে যায়। ডিবাগিংয়ে এর মূল্য অপরিসীম। আধুনিক ভাষাগুলো (Rust, Go, Swift, C#) সবাই এই পথ নিয়েছে।

মূল উপলব্ধি: নিরাপত্তা ভাষার নিয়মে রাখুন, আর গতির দায়িত্ব দিন compiler-কে। সীমা যাচাই হলো খুব কম দামে কেনা একটি বিশাল বীমা — আর স্মার্ট JIT সেই দামও প্রায় শূন্যে নামিয়ে আনে।

প্র ০২String[] কি একটি Object[]? Array covariance আর ArrayStoreException-কে কেন Java-র একটি নকশা-ভুল বলা হয়?

Java-তে String হলো Object-এর একটি উপ-টাইপ (পর্ব ১৯-এর inheritance). Java ঠিক করেছিল, তাহলে String[]-ও হবে Object[]-এর উপ-টাইপ। একে বলে array covariance. তাই এটি বৈধ: Object[] objs = new String[3];. সমস্যা শুরু হয় এর পরের লাইনে: objs[0] = 42;. Compiler-এর চোখে objs একটি Object[], আর ৪২ (Integer) একটি Object — তাই compile হয়ে যায়। কিন্তু চলার সময় array-টি আসলে String[]; তাতে Integer রাখা যায় না। JVM ধরে ফেলে, আর ছোড়ে java.lang.ArrayStoreException: java.lang.Integer.

অর্থাৎ একটি টাইপ-ভুল, যা compile-এর সময় ধরা উচিত ছিল, তা চলার সময় পর্যন্ত গড়ায়। আর এজন্য JVM-কে reference-array-এর প্রতিটি লেখার সময় বাড়তি একটি টাইপ-যাচাই করতে হয় — একটি স্থায়ী খরচ।

তাহলে এমন নকশা কেন? ১৯৯৫-৯৬ সালে Java-তে generics ছিল না (এসেছে Java 5-এ, ২০০৪ সালে)। তখন যেকোনো টাইপের array সাজাতে পারে এমন একটি method লিখতে, যেমন Arrays.sort(Object[] a), covariance ছাড়া উপায় ছিল না — নইলে String[], Integer[] প্রতিটির জন্য আলাদা method লাগত। Covariance ছিল generics-এর অভাবে একটি বাস্তব সমঝোতা।

পরে যখন generics এলো, নকশাকারেরা এই ভুল আর করেননি: List<String> কোনোভাবেই List<Object> নয় (একে বলে invariance), তাই List<Object> objs = new ArrayList<String>(); compile-ই হয় না। ভুলটি এবার compile-এর সময়ই ধরা পড়ে। Java Collections-এর নকশাকার Joshua Bloch তাঁর Effective Java বইয়ে সরাসরি পরামর্শ দেন: array-এর চেয়ে list পছন্দ করুন, কারণ array covariant আর list-এর generics টাইপের ভুল আগেই ধরে। C#-ও Java-কে অনুসরণ করে array covariance রেখেছিল; আজ ভাষা-নকশাকারদের কেউ কেউসহ অনেকেই একে নকশার ভুল হিসেবে দেখেন।

পর্ব ২৮-এর generics-এ আপনি ? extends wildcard দেখবেন — covariance-এর সুবিধা, কিন্তু নিরাপদভাবে: পড়া যায়, ভুল টাইপ লেখা যায় না।

মূল উপলব্ধি: ভাষার নকশা হয় সেই সময়ের সীমাবদ্ধতার ভেতরে। একটি সিদ্ধান্ত একবার মুক্তি পেলে পেছনের সামঞ্জস্যের জন্য তা চিরকাল থেকে যায় — তাই টাইপ-নিরাপত্তায় নতুন কোডে array-এর বদলে generics-এর collection ভালো।

প্র ০৩Array-এর দৈর্ঘ্য length, String-এর length(), ArrayList-এর size() — একই ভাষায় তিন রকম কেন?

প্রথম দেখায় এটা নিছক অসংগতি মনে হয়, আর আংশিকভাবে তা সত্য — তবে পেছনের কারণগুলো শেখার মতো।

Array কোনো সাধারণ class নয়। Array টাইপ (int[], String[]) কোনো .java ফাইলে লেখা হয়নি; JVM নিজে এদের বানায়। Array-এর দৈর্ঘ্য তৈরির মুহূর্তে স্থির হয়ে object-এর header-এর পাশে লেখা থাকে, আর কখনো বদলায় না। Bytecode-এ এর জন্য একটি আলাদা নির্দেশই আছে — arraylength. Java ভাষা এটিকে একটি public final field-এর মতো দেখায়, কারণ এটি সত্যিই একটি স্থির, সরাসরি পড়া যায় এমন মান — কোনো হিসাব লাগে না। (মজার ব্যাপার, reflection দিয়ে array-এর field খুঁজলে length পাওয়া যায় না — এটি আসলে ভাষার বিশেষ ব্যবস্থা।)

String একটি সাধারণ class, আর Java-র মূল নীতি encapsulation (পর্ব ১৭): ভেতরের ডেটা লুকানো, বাইরে method. এই নীতির মূল্য ইতিহাসেই প্রমাণ হয়েছে। Java 9-এ (JEP 254, compact strings) String-এর ভেতরের রূপ বদলে char[] থেকে byte[] করা হয়, যাতে শুধু ইংরেজি অক্ষরের লেখা অর্ধেক মেমরি নেয়। এখন বাংলার মতো লেখায় ভেতরের byte-সংখ্যা অক্ষর-সংখ্যার দ্বিগুণ। length যদি public field হতো, এই বদল লক্ষ লক্ষ প্রোগ্রাম ভেঙে দিত; method হওয়ায় শুধু length()-এর ভেতরের হিসাব বদলালেই কাজ শেষ।

ArrayList-এর আকার বদলায়, আর Collections framework (Java 1.2, ১৯৯৮) সব collection-এর জন্য একটিই নাম বেছে নেয়: size() — List, Set-সহ সব Collection-এর জন্য এক interface, আর Map-এও একই নাম। “length” শব্দটি স্থির কিছু বোঝায়, “size” বোঝায় এখন কতগুলো আছে। Collections আসার আগে array আর String-এর নাম ঠিক হয়ে গিয়েছিল, আর পেছনের সামঞ্জস্যের জন্য সেগুলো আর বদলানো যায়নি।

অনুশীলনের পরামর্শ: স্থির জিনিস (array) → field; লুকানো ভেতরের রূপ (String) → method; বদলায় এমন collection → size(). IDE-র auto-complete এ নিয়ে ভুল করতে দেয় না, কিন্তু কাগজে লেখা পরীক্ষায় এ নিয়ে প্রশ্ন আসতে পারে।

মূল উপলব্ধি: public field একটি চিরস্থায়ী প্রতিশ্রুতি; method ভবিষ্যতে ভেতরটা বদলানোর স্বাধীনতা দেয়। Array-এর length বদলানোর কোনো দরকার কখনো আসবে না বলেই সেটি field হতে পেরেছে।

প্র ০৪Index কেন ০ থেকে শুরু? মানুষ তো ১ থেকে গোনে — Dijkstra ১৯৮২ সালে কী যুক্তি দিয়েছিলেন?

সব ভাষা ০ থেকে গোনে না। Fortran (১৯৫৭) গুনত ১ থেকে, আজও MATLAB, R, Lua আর Julia ১ থেকে গোনে। Java ০ বেছে নিয়েছে মূলত উত্তরাধিকার থেকে: Java-র ব্যাকরণ ইচ্ছা করেই C-র মতো রাখা হয়েছিল, যাতে C/C++ প্রোগ্রামাররা সহজে আসতে পারেন। আর C ০ নিয়েছিল তার পূর্বসূরি BCPL থেকে, যেখানে কারণটি ছিল যান্ত্রিক: index মানে শুরুর ঠিকানা থেকে কত ঘর দূরে। a[i] মানে ঠিকানা a + i — প্রথম ঘর a + 0. ১ থেকে গুনলে প্রতিবার একটি বিয়োগ লাগত।

কিন্তু ০-এর পক্ষে সবচেয়ে বিখ্যাত যুক্তিটি যন্ত্রের নয়, গণিতের। ১৯৮২ সালের আগস্টে ডাচ কম্পিউটার বিজ্ঞানী Edsger W. Dijkstra হাতে লেখা একটি ছোট নোট লেখেন, শিরোনাম “Why numbering should start at zero” (তাঁর নোট সিরিজের EWD831). তিনি প্রশ্ন করেন: ২, ৩, … ১২ — এই সংখ্যার সারি লেখার সবচেয়ে ভালো উপায় কী? চারটি বিকল্প: 2 ≤ i < 13, 1 < i ≤ 12, 2 ≤ i ≤ 12, 1 < i < 13. তাঁর যুক্তি, প্রথমটি সেরা — নিচের সীমা অন্তর্ভুক্ত, ওপরের সীমা বাদ। এতে সারির দৈর্ঘ্য হয় সরাসরি দুই সীমার বিয়োগফল (১৩ − ২ = ১১), পাশাপাশি দুটি সারি জোড়া দেওয়া যায় ফাঁক বা ওভারল্যাপ ছাড়া ([a, b) আর [b, c)), আর খালি সারি লেখা যায় স্বাভাবিকভাবে (a ≤ i < a).

এখন N ঘরের একটি সারির index এই নিয়মে লিখলে: ১ থেকে শুরু করলে 1 ≤ i < N+1, ০ থেকে শুরু করলে 0 ≤ i < N — দ্বিতীয়টি অনেক পরিষ্কার। Java-র লুপ for (int i = 0; i < a.length; i++) হুবহু এই রূপ। আর Java-র library জুড়ে একই নীতি: substring(begin, end), Arrays.copyOfRange(a, from, to), List.subList — সবখানে শুরু অন্তর্ভুক্ত, শেষ বাদ।

০ থেকে গোনার আরেকটি সুবিধা দেখা যায় ভাগশেষে: i % n সবসময় ০ থেকে n−১, তাই চক্রাকার বাফার বা সপ্তাহের দিন ঘোরাতে কোনো +১/−১ লাগে না।

মূল উপলব্ধি: ০ থেকে গোনা কোনো খামখেয়ালি নয় — “শুরু অন্তর্ভুক্ত, শেষ বাদ” নিয়মের সাথে মিলে এটি off-by-one ভুল কমায়। এই নিয়মটি একবার আত্মস্থ করলে Java-র প্রায় প্রতিটি range-ওয়ালা method আগে থেকেই অনুমান করতে পারবেন।

প্র ০৫Arrays.sort ভেতরে কোন অ্যালগরিদম চালায়? primitive আর object-এর জন্য দুটি আলাদা অ্যালগরিদম কেন?

Arrays.sort(int[]) আর Arrays.sort(String[]) দেখতে একই রকম, কিন্তু Java 7 থেকে ভেতরে দুটি সম্পূর্ণ আলাদা অ্যালগরিদম চলে — আর এর পেছনে একটি সুন্দর প্রকৌশল-যুক্তি আছে।

Primitive-এর জন্য Dual-Pivot Quicksort. সাধারণ quicksort একটি pivot বেছে array-কে দুই ভাগে ভাগ করে। ২০০৯ সালে রুশ প্রোগ্রামার Vladimir Yaroslavskiy দুটি pivot নিয়ে তিন ভাগে ভাগ করার একটি সংস্করণ প্রস্তাব করেন; Jon Bentley আর Joshua Bloch-এর সাথে মিলে এটি পরিমার্জিত হয়ে Java 7-এ primitive array-এর ডিফল্ট হয়। বাস্তব পরীক্ষায় এটি আগের ক্লাসিক quicksort-এর চেয়ে দ্রুত চলেছে, আংশিকভাবে আধুনিক CPU-র cache ভালোভাবে ব্যবহার করার কারণে। ছোট টুকরোতে এটি insertion sort-এ চলে যায়, আর পরের version-গুলোতে প্রায়-সাজানো ডেটা আর খারাপতম ক্ষেত্র সামলানোর জন্য আরও উন্নত করা হয়েছে।

Object-এর জন্য TimSort. TimSort ২০০২ সালে Tim Peters Python-এর জন্য বানিয়েছিলেন; Java 7-এ object array আর Collections.sort-এর জন্য এটি গৃহীত হয়। এটি merge sort আর insertion sort-এর মিশ্রণ, যা বাস্তব ডেটায় আগে থেকেই সাজানো টুকরো (run) খুঁজে নিয়ে কাজে লাগায় — প্রায়-সাজানো তালিকায় প্রায় এক পাকেই কাজ শেষ।

দুটি আলাদা কেন? মূল কারণ stability. একটি sort stable হলে সমান মানের উপাদানগুলোর আগের ক্রম বজায় থাকে। ধরুন শিক্ষার্থীদের আগে নাম অনুযায়ী সাজিয়েছেন, তারপর নম্বর অনুযায়ী — stable sort হলে একই নম্বরের শিক্ষার্থীরা নামের ক্রমেই থাকবে। Object-এর ক্ষেত্রে এটা জরুরি, কারণ দুটি “সমান” object-এর অন্য field আলাদা হতে পারে। Quicksort stable নয়। কিন্তু primitive-এর ক্ষেত্রে দুটি সমান ৩৫ সম্পূর্ণ অভিন্ন — কোনটা আগে, কোনটা পরে, বোঝারই উপায় নেই। তাই সেখানে stability-র দাম না দিয়ে দ্রুততম অ্যালগরিদম বেছে নেওয়া যায়। আরেকটি কারণ: object তুলনা (compareTo, পর্ব ৩৩) দামি method কল, আর TimSort তুলনার সংখ্যা কমাতে বিশেষভাবে দক্ষ।

একটি বাস্তব ঘটনা: TimSort-এ যদি এমন Comparator দেওয়া হয় যা নিজের নিয়মই ভাঙে (যেমন a < b আর b < a দুটোই সত্য বলে), Java 7 থেকে মাঝে মাঝে আসে IllegalArgumentException: Comparison method violates its general contract! — পুরোনো অ্যালগরিদম এসব ভুল চুপচাপ মেনে নিত। Java 8 থেকে বড় array-এর জন্য Arrays.parallelSort-ও আছে, যা একাধিক CPU core ব্যবহার করে।

মূল উপলব্ধি: “সেরা অ্যালগরিদম” বলে কিছু নেই — আছে ডেটার ধরন অনুযায়ী সেরা। একই method-এর পেছনে ডেটা বুঝে আলাদা কৌশল বেছে নেওয়াই ভালো library নকশা।

অনুশীলন

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

  1. আউটপুট অনুমান: চালানোর আগে কাগজে লিখুন কী ছাপবে — int[] a = {1, 2, 3}; int[] b = a.clone(); int[] c = a; c[0] = 7; b[1] = 8; তারপর Arrays.toString(a), Arrays.toString(b), আর a == c, a == b.

    a = [7, 2, 3] — কারণ c আর a একই array, c[0] = 7 সেটিকেই বদলেছে। b = [1, 8, 3] — clone() আলাদা array বানিয়েছিল, তাই b[1] = 8 শুধু b-তে, আর c[0] = 7 b-তে পৌঁছায়নি। a == c → true (একই object), a == b → false (আলাদা object, মান মিললেও না)।

  2. Shallow copy-র ফাঁদ: int[][] t = {{1, 2}, {3, 4}}; int[][] u = t.clone(); u[0][0] = 9; u[1] = new int[]{5, 6}; — এখন Arrays.deepToString(t) কী ছাপবে? stack-heap ছবি এঁকে ব্যাখ্যা করুন।

    [[9, 2], [3, 4]]. t.clone() শুধু বাইরের array কপি করেছে — u একটি নতুন বাইরের array, কিন্তু তার দুটি ঘরে t-এর মতোই একই দুটি সারির reference. তাই u[0][0] = 9 শেয়ার করা সারি০ বদলেছে, যা t-ও দেখে। কিন্তু u[1] = new int[]{5, 6} বদলেছে শুধু u-এর বাইরের array-এর একটি ঘর; t[1] এখনো পুরোনো {3, 4} দেখায়। স্বাধীন কপির জন্য লিখতে হতো প্রতিটি সারির জন্য u[i] = t[i].clone();.

  3. একটি method লিখুন: static int topScorer(int[] marks) — সর্বোচ্চ নম্বর পাওয়া শিক্ষার্থীর index ফেরত দেবে (সমান হলে প্রথমজন), খালি array হলে -1. তারপর {78, 91, 64, 91}, {-5, -2, -8} আর {} দিয়ে পরীক্ষা করুন।
    static int topScorer(int[] marks) {
        if (marks == null || marks.length == 0) return -1;
        int best = 0;                          // সেরার index, মান নয়
        for (int i = 1; i < marks.length; i++) {
            if (marks[i] > marks[best]) best = i;   // > দিলে সমান হলে প্রথমজন থাকে
        }
        return best;
    }

    ফল: {78, 91, 64, 91} → ১ (দুজন ৯১ পেয়েছে, প্রথমজন index ১-এ; >= দিলে আসত ৩), {-5, -2, -8} → ১ (max ০ দিয়ে শুরু করলে এখানে ভুল হতো), {} → -১. এখানে index লাগছে বলে for-each নয়, index-ওয়ালা for.

  4. AI অডিট: যেকোনো AI-কে শুধু এটুকু বলুন: “Java-তে একটি int array-এর সব উপাদান ছাপো, সর্বোচ্চ মান বের করো, আর দুটি array সমান কি না দেখো।” উত্তরটি অংশ ৯-এর টেবিল আর চেকলিস্ট দিয়ে যাচাই করুন।

    প্রায়ই দেখা যায়:

    • int max = 0; দিয়ে শুরু — ঋণাত্মক মানের array-এ ভুল উত্তর দেবে।
    • সমান কি না দেখতে == বা a.equals(b) — array-এর equals আসলে Object-এর, যা ==-এর মতোই আচরণ করে। সঠিক হলো Arrays.equals.
    • খালি array-এর কথা কোথাও ভাবা হয়নি — arr[0] সেখানে exception দেবে।

    তারপর অংশ ৯-এর prompt-এর মতো শর্ত জুড়ে আবার জিজ্ঞেস করুন, আর দুটি উত্তর পাশাপাশি রেখে তুলনা করুন।

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

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

ক্রমএকটি Java array-এর জীবনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 int[] marks; — শুধু reference রাখার ভ্যারিয়েবল ঘোষণা
  2. 🏗️ new int[5] — heap-এ ৫ ঘরের array, সব ঘরে 0
  3. ✏️ marks[i] = … — index দিয়ে ঘরে মান লেখা
  4. 🛡️ প্রতিটি পড়া-লেখায় JVM-এর সীমা যাচাই
  5. 🗑️ কোনো reference না থাকলে garbage collector জায়গা ফেরত নেয়

Java-তে int[] a = new int[4]; লেখার পর a[2]-এর মান কত?

  • 0
  • null
  • অনির্দিষ্ট আবর্জনা মান
  • compile error, কারণ মান দেওয়া হয়নি

Java-তে arr নামের একটি array-এর উপাদানের সংখ্যা জানার সঠিক উপায় কোনটি?

  • arr.length()
  • arr.size()
  • arr.length
  • length(arr)

int[] a = {1, 2, 3}; int[] b = a; b[1] = 9; System.out.println(a[1]); — কী ছাপবে?

  • 2
  • 9
  • ArrayIndexOutOfBoundsException
  • [I@ দিয়ে শুরু হওয়া একটি লেখা

দুটি আলাদাভাবে তৈরি Java array x আর y, দুটিতেই {5, 6}. কোনটি true দেবে?

  • x == y
  • x.equals(y)
  • x.toString().equals(y.toString())
  • Arrays.equals(x, y)

Java-তে int[] a = new int[3]; a[3] = 1; — কী ঘটবে?

  • compile error
  • পাশের মেমরিতে 1 লেখা হবে, কোনো বার্তা ছাড়া
  • চলার সময় ArrayIndexOutOfBoundsException
  • array আপনাআপনি ৪ ঘরের হয়ে যাবে

Arrays.binarySearch সঠিক ফল দেওয়ার পূর্বশর্ত কী?

  • array-এর দৈর্ঘ্য জোড় সংখ্যা হতে হবে
  • array আগে থেকে ছোট থেকে বড় সাজানো থাকতে হবে
  • array-এ কোনো ডুপ্লিকেট থাকা চলবে না
  • array অবশ্যই String[] হতে হবে

শব্দকোষ

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

মনে রাখুন

  1. Array = নির্দিষ্ট সংখ্যক একই টাইপের ঘর, index ০ থেকে length − ১; তৈরির পর আকার স্থির, নতুন ঘরে default value.
  2. Java-র array heap-এর object: নিজের length জানে (field), আর প্রতিটি access-এ সীমা যাচাই — ভুল index মানে ArrayIndexOutOfBoundsException.
  3. b = a শুধু reference কপি করে; আলাদা কপি চাইলে clone, Arrays.copyOf, System.arraycopy — আর এগুলো shallow.
  4. ছাপতে Arrays.toString, তুলনায় Arrays.equals; binarySearch-এর আগে sort.
  5. 2D array = array-এর array, সারিগুলো আলাদা object (jagged হতে পারে); আকার অজানা হলে ArrayList.

পরের পর্বে: মডিউল ২-এর শেষ পর্বে লেখা খোঁজার সবচেয়ে শক্তিশালী হাতিয়ার — regular expression: ফোন নম্বর বা ইমেইল যাচাই, লগ থেকে তথ্য বের করা, আর Pattern/Matcher দিয়ে লেখা কাটা-বদলানো।

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

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