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

List — ArrayList আর LinkedList, বাড়তে পারা তালিকার ভেতর-বাহির

Lists in Java — ArrayList, LinkedList, Iteration, Big-O, Immutable Lists and Views

Taught in Bangla, this lesson covers Java lists: the Collections Framework map, interface versus implementation, core List methods and the remove(int) versus remove(Object) trap, how ArrayList grows with amortized O(1) adds, why LinkedList usually loses on cache locality, safe removal while iterating, and immutable lists versus views like subList.

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

  • Collections Framework-এর মানচিত্র — Collection, List, Set, Queue আর আলাদা Map; interface বনাম implementation
  • List-এর মূল কাজ (add, get, set, remove, contains, indexOf) আর remove(int) বনাম remove(Object)-এর ফাঁদ
  • ArrayList-এর ভেতরের array, capacity বনাম size, বৃদ্ধি আর amortized O(1); LinkedList-এর node আর cache locality
  • ঘুরে দেখার চার উপায়, ঘোরার মধ্যে বাদ দিলে কী ভাঙে, আর Iterator.remove ও removeIf দিয়ে সমাধান
  • List.of, List.copyOf, unmodifiableList, Arrays.asList, subList — কোনটা কপি, কোনটা view; সাজানো আর Collections-এর কাজের method

১ · সমস্যা — array-এর মাপ আগে থেকে জানতে হয়

পর্ব ১৩-এ array দেখেছেন: new int[5] লিখলে ঠিক পাঁচটি ঘর, তার বেশি নয়, কমও নয়। পর্ব ২২-এর “হিসাব খাতা”-য় এই সীমাবদ্ধতার সাথে সরাসরি লড়তে হয়েছিল। সেখানে একটি বড় array রাখা হয়েছিল, সাথে একটি count — কয়টি ঘর আসলে ভরা। খাতা ভরে গেলে add শুধু false ফেরত দিত। কম ঘর ধরলে খাতা দ্রুত ভরে, বেশি ধরলে মেমরি নষ্ট। আর মাঝের একটি খরচ মুছতে চাইলে বাকি সবাইকে হাতে এক ঘর করে সরাতে হতো।

বাস্তবের বেশিরভাগ তালিকার মাপ আগে থেকে জানা থাকে না। একটি অনলাইন দোকানের shopping cart-এ গ্রাহক কয়টি জিনিস রাখবেন? আজ ক্লাসে কয়জন উপস্থিত? একটি bKash অ্যাকাউন্টে আজ কয়টি লেনদেন হবে? দরকার এমন একটি তালিকা, যা:

  • ক্রম মনে রাখে — যে ক্রমে ঢুকিয়েছেন, সেই ক্রমেই থাকে;
  • index দিয়ে পৌঁছানো যায় — array-এর মতোই ০ থেকে গোনা;
  • দরকার মতো বাড়ে-কমে — যোগ করলে বড় হয়, বাদ দিলে ছোট হয়, বাকিদের সরানোর কাজ নিজেই করে।

Java-তে এমন তালিকার নাম List. আর সবচেয়ে বেশি ব্যবহৃত List হলো ArrayList — মজার ব্যাপার, ভেতরে সে ঠিক আপনার হিসাব খাতার মতোই একটি array আর একটি count রাখে। পার্থক্য একটাই: ভরে গেলে সে নিজেই একটি বড় array বানিয়ে নেয়। এমন নিজে বড় হওয়া array-কে বলে dynamic array.

ক্লাসের হাজিরা খাতা ভাবুন। ছাপানো খাতায় ঠিক ৬০টি লাইন — ৬১ নম্বর ছাত্র ভর্তি হলে আর জায়গা নেই। এটা array. এবার ভাবুন একজন সহকারী শিক্ষক, যাঁর কাছে খাতা ভরে গেলে তিনি নিজে থেকে একটি বড় খাতা আনেন, পুরোনো সব নাম নতুন খাতায় তুলে দেন, তারপর নতুন নামটি লেখেন। আপনি শুধু বলেন “এই নামটি যোগ করুন” — খাতা বদলের ঝামেলা আপনার নয়। এটা ArrayList. আর আপনি সেই সহকারীকে যে নির্দেশগুলো দিতে পারেন (যোগ করুন, ৫ নম্বর নামটি বলুন, এই নামটি কেটে দিন) — সেই নির্দেশের তালিকাটাই List interface.

২ · Collections Framework-এর মানচিত্র — interface এক, implementation অনেক

List একা নয়। Java-র java.util প্যাকেজে ডেটা রাখার একটি পুরো পরিবার আছে, যাকে বলে Collections Framework. পরের কয়েকটি পর্ব এই পরিবারের সদস্যদের নিয়েই। আগে মানচিত্রটা দেখে নিন:

Collections Framework — মূল interface আর পরিচিত class

Interfaces on top, implementations below; Map stands apart

          Iterable<E>
              │
         Collection<E>
     ┌────────┼─────────┐
  List<E>   Set<E>   Queue<E>
     │        │         │
 ArrayList  HashSet  ArrayDeque
 LinkedList TreeSet  PriorityQueue
              (LinkedList-ও Deque)

   Map<K,V>   ← আলাদা, Collection নয়
 HashMap  TreeMap  LinkedHashMap
ছবি ২৯.১ — ওপরের সারিগুলো interface (কী করা যায়), নিচের নামগুলো class (কীভাবে করা হয়)। List এই পর্বে, Set আর Map পর্ব ৩১-এ, Queue পর্ব ৩২-এ।

সবার চূড়ায় Iterable — যে জিনিসের ওপর for-each লুপ চালানো যায় (পর্ব ১১)। তার নিচে Collection: যেকোনো “অনেকগুলো জিনিসের দল”, যাতে add, remove, contains, size, isEmpty আছে। Collection থেকে তিনটি শাখা:

List

ক্রম মানে, index আছে, একই জিনিস একাধিকবার রাখা যায়। “৩ নম্বর জিনিসটা দাও” — এই প্রশ্নের উত্তর দেয়।

shopping cart, হাজিরা, লেনদেনের ইতিহাস

Set

একই জিনিস দুবার নয়; “এটা কি আছে?” প্রশ্নে খুব দ্রুত। index নেই।

ইউনিক রোল নম্বর, ট্যাগ — পর্ব ৩১

Queue / Deque

এক প্রান্তে ঢোকে, অন্য প্রান্ত (বা অগ্রাধিকার) মেনে বের হয়।

টিকিটের লাইন, প্রিন্টের কাজ — পর্ব ৩২

আর Map — key দিয়ে মান খোঁজা (রোল → নাম) — ইচ্ছা করেই Collection-এর বাইরে, কারণ তার প্রতিটি উপাদান একটি জোড়া, একক জিনিস নয়। এর কাহিনি পর্ব ৩১-এ।

বাঁয়ে interface, ডানে implementation

এই নকশার সবচেয়ে বড় শিক্ষা হলো কী করা যায় (interface) আর কীভাবে করা হয় (implementation) আলাদা। তাই List তৈরির প্রচলিত লেখা:

List<String> names = new ArrayList<>();     // ভ্যারিয়েবলের টাইপ: interface
List<String> queue = new LinkedList<>();    // একই interface, অন্য implementation

বাঁ দিকে List, ডান দিকে ArrayList. পর্ব ২১-এর program to an interface নীতি এখানে সবচেয়ে বেশি কাজে লাগে। আপনার বাকি কোড শুধু List-এর method ডাকে, তাই কাল যদি মাপজোখ করে দেখেন অন্য implementation ভালো, শুধু ডান দিকের একটি শব্দ বদলালেই চলবে। Method-এর parameter আর return টাইপেও একই নিয়ম: double average(List<Integer> marks) লিখুন, ArrayList<Integer> নয় — তাহলে যে ডাকে, সে যেকোনো List দিতে পারে, List.of(...)-ও। কৌণিক বন্ধনীর <String> হলো পর্ব ২৮-এর generics — তালিকায় শুধু String ঢুকবে, compiler তা পাহারা দেবে।

৩ · মূল কাজগুলো — আর একটি বিখ্যাত ফাঁদ

List-এর method অনেক, কিন্তু প্রতিদিনের কাজ চলে দশটির মতো দিয়ে:

Methodকী করেমনে রাখার কথা
add(x)শেষে যোগসবসময় true ফেরত দেয়
add(i, x)index i-তে ঢোকায়, পরের সবাই এক ঘর ডানেi থাকতে হবে ০ থেকে size()-এর মধ্যে
get(i)index i-এর জিনিসভুল index → IndexOutOfBoundsException
set(i, x)index i-এর জিনিস বদলায়পুরোনো জিনিসটা ফেরত দেয়
remove(int i)index i-এর জিনিস বাদবাদ পড়া জিনিস ফেরত দেয়
remove(Object o)প্রথম যে জিনিসটি o-এর সমান, সেটা বাদপেলে true; সমান মানে equals
size(), isEmpty()কয়টি আছে, খালি কি নাarray-এর length-এর বদলে
contains(o), indexOf(o)আছে কি না, কোথায় (না থাকলে −১)শুরু থেকে একে একে equals দিয়ে খোঁজে
clear()সব বাদsize হয় ০
getFirst(), getLast(), reversed()প্রথম, শেষ, উল্টো ক্রমের viewJava 21 থেকে; খালি তালিকায় NoSuchElementException

শেষ সারিটি নতুন। Java 21-এ SequencedCollection নামে একটি interface যোগ হয়েছে (JEP 431), যাতে list.get(list.size() - 1)-এর মতো বিরক্তিকর লেখার বদলে সরাসরি list.getLast() লেখা যায়। পুরোনো বই বা টিউটোরিয়ালে এগুলো পাবেন না।

contains, indexOf আর remove(Object) “সমান” বোঝে equals() দিয়ে, == দিয়ে নয়। তাই String বা Integer-এর তালিকায় এরা ঠিকঠাক কাজ করে। কিন্তু আপনার নিজের class-এ যদি পর্ব ২৩-এর মতো equals override না করেন, তাহলে হুবহু একই তথ্যের দুটি object-ও “আলাদা” — contains মিথ্যা বলবে। record-এ equals নিজে থেকেই তৈরি হয়, তাই এই সমস্যা নেই।

এবার একটি ছোট মুদি দোকানের shopping cart, আর শেষে রোল নম্বরের তালিকায় সেই বিখ্যাত ফাঁদ। চালিয়ে দেখুন:

import java.util.*;
class Main {
    public static void main(String[] args) {
        List<String> cart = new ArrayList<>();
        cart.add("চাল");
        cart.add("ডাল");
        cart.add("তেল");
        cart.add(1, "লবণ");                  // index 1-এ ঢোকানো
        System.out.println(cart + " size=" + cart.size());
        cart.set(3, "সয়াবিন তেল");           // index 3-এর জিনিস বদল
        cart.remove("ডাল");                  // মান ধরে বাদ
        System.out.println(cart.get(0) + ", " + cart.indexOf("সয়াবিন তেল") + ", " + cart.contains("ডাল"));
        System.out.println(String.join(" | ", cart));
        List<Integer> rolls = new ArrayList<>(List.of(3, 7, 1));
        rolls.remove(1);                     // index 1 — মান 1 নয়!
        System.out.println(rolls);
        rolls.remove(Integer.valueOf(1));    // এবার মান 1
        System.out.println(rolls + " " + rolls.isEmpty());
    }
}

আউটপুট পাঁচ লাইন: [চাল, লবণ, ডাল, তেল] size=4, চাল, 2, false, চাল | লবণ | সয়াবিন তেল, [3, 1] আর [3] false. add(1, "লবণ") লবণকে দ্বিতীয় ঘরে বসাল আর ডাল-তেলকে এক ঘর ডানে ঠেলল — হাতে কিছু সরাতে হয়নি। List-কে সরাসরি println করলে সুন্দর […] রূপ আসে (array-এ যা আসত না, পর্ব ১৩)। এবার রোলের তালিকা [3, 7, 1]: উদ্দেশ্য ছিল “রোল ১ ক্লাস ছেড়েছে, বাদ দাও”, কিন্তু rolls.remove(1) বাদ দিল index 1-এর জিনিস, অর্থাৎ ৭! কারণ নিচে। (টীকা: অনলাইন runner ফাইলটিকে অন্য নামে compile করে, তাই Main এখানে public নয়; নিজের প্রজেক্টে public class Main থাকে Main.java ফাইলে।)

remove(1)-এর ফাঁদ। List-এ দুটি remove আছে: remove(int index) আর remove(Object o). পর্ব ১২-এর overload resolution-এর নিয়মে compiler আগে খোঁজে boxing ছাড়া মেলে এমন method. 1 একটি int, তাই remove(int) জিতে যায় — autoboxing করে remove(Object) পর্যন্ত যাওয়ার দরকারই পড়ে না। List<String>-এ এই সমস্যা নেই, কারণ সেখানে মান আর index দেখতে আলাদা। কিন্তু List<Integer>-এ দুটোই সংখ্যা। মান ধরে বাদ দিতে চাইলে স্পষ্ট করে object দিন: remove(Integer.valueOf(1)). কোনো exception আসে না, ভুল জিনিস চুপচাপ মুছে যায় — তাই এটা বিপজ্জনক।

ভুল index দিলে List array-এর মতোই সঙ্গে সঙ্গে থামে। [3, 7, 1]-এ rolls.get(5) ডাকলে বার্তা হয় java.lang.IndexOutOfBoundsException: Index 5 out of bounds for length 3 — এটা IndexOutOfBoundsException, array-এর ArrayIndexOutOfBoundsException-এর বড় ভাই।

৪ · ArrayList-এর ভেতরে — capacity, size আর দেড় গুণ বৃদ্ধি

OpenJDK-র ArrayList-এর source খুললে দুটি মূল field পাবেন: Object[] elementData আর int size. অর্থাৎ পর্ব ২২-এর items আর count. এখানে দুটি শব্দ আলাদা করে বোঝা জরুরি:

  • size — তালিকায় আসলে কয়টি জিনিস আছে। size() এটাই ফেরত দেয়।
  • capacity — ভেতরের array-তে মোট কয়টি ঘর আছে, ভরা হোক বা খালি। এটা দেখার কোনো public method নেই; এটা ArrayList-এর গোপন ব্যাপার।

Size যতক্ষণ capacity-র কম, শেষে যোগ মানে শুধু elementData[size++] = x — একটি লেখা। ভরে গেলে কী হয়, ধাপে ধাপে দেখুন:

1List<String> cart = new ArrayList<>();2cart.add("চাল");3// আরও ৯টি add — মোট ১০4cart.add("চিনি");    // ১১তম5// ভেতরে: grow() → নতুন array6// ১৬তম, ২৩তম add-এ আবার
ArrayList-এর ভেতরটা
size
capacity
এ পর্যন্ত মোট কপি
  1. খালি তালিকা, খালি array. new ArrayList<>() তৈরির সময় আধুনিক OpenJDK কোনো ঘরই বানায় না — সব খালি তালিকা একটি ভাগাভাগি করা শূন্য-মাপের array দেখায়। অনেক তালিকা তৈরি হয়ে খালিই থেকে যায়; তাদের জন্য মেমরি নষ্ট হয় না।
  2. প্রথম add-এ ১০ ঘর। প্রথম জিনিস আসামাত্র ডিফল্ট capacity ১০-এর একটি array তৈরি হয়। size ১, capacity ১০।
  3. দশম add পর্যন্ত সস্তা। প্রতিটি add শুধু পরের খালি ঘরে লেখে আর size বাড়ায়। দশটির পর size আর capacity সমান — array ভরা।
  4. এগারোতম add — বড় হওয়া। জায়গা নেই, তাই ArrayList নতুন capacity হিসাব করে: পুরোনো + পুরোনোর অর্ধেক (10 + (10 >> 1) = 15). তারপর Arrays.copyOf দিয়ে ১৫ ঘরের নতুন array বানিয়ে পুরোনো ১০টি reference কপি করে। জিনিসগুলো নিজে (String object) কপি হয় না, শুধু তাদের ঠিকানা।
  5. নতুন জিনিস লেখা, পুরোনো array ফেলে দেওয়া। “চিনি” বসে ঘর ১০-এ, size ১১। পুরোনো ১০ ঘরের array-কে আর কেউ দেখায় না — garbage collector পরে সেটা সরিয়ে নেবে।
  6. জ্যামিতিক বৃদ্ধির জাদু। capacity চলে ১০ → ১৫ → ২২ → ৩৩ → ৪৯ → ৭৩… ২৩টি add-এ মোট কপি ১০ + ১৫ + ২২ = ৪৭, অর্থাৎ গড়ে প্রতি add-এ ২টিরও কম কপি। n যত বড়ই হোক, এই গড় একটি ধ্রুবকের নিচে থাকে।

ধাপ ৬-এর হিসাবকে বলে amortized analysis: কোনো একটি add মাঝে মাঝে ব্যয়বহুল (কপির সময়), কিন্তু অনেকগুলো add-এর মোট খরচ ভাগ করলে প্রতিটির ভাগে ধ্রুব খরচ। তাই বলা হয়, ArrayList-এর শেষে যোগ amortized O(1). এখানে O(1), O(n) লেখার রীতিটিকে বলে Big-O notation: n (তালিকার আকার) বাড়লে কাজের সময় কোন হারে বাড়ে। O(1) মানে তালিকা দশটির হোক বা দশ লাখের, প্রায় একই সময়; O(n) মানে আকারের সমানুপাতে।

মাপ জানা থাকলে আগেই বলুন

যদি জানেন প্রায় ৫০,০০০ লেনদেন আসবে, লিখুন new ArrayList<>(50_000). এই সংখ্যা হলো শুরুর capacity, size নয় — তালিকা তবু খালি (size() ০), শুধু মাঝপথে বারবার বড় হওয়ার কপিগুলো বেঁচে যায়। new ArrayList<>(100).get(0) তাই IndexOutOfBoundsException দেয় — খুব সাধারণ একটি বিভ্রান্তি। আর সব কাজ শেষে অতিরিক্ত খালি ঘর ছাঁটতে চাইলে আছে trimToSize(), যদিও কদাচিৎ লাগে।

শেষে যোগ সস্তা, শুরুতে ঢোকানো দামি

Array পাশাপাশি ঘরে বসে (contiguous memory), তাই get(i) একটি গুণ আর একটি যোগে ঠিকানা পায় — O(1). কিন্তু এই পাশাপাশি থাকারই দাম আছে। add(0, x) করতে হলে আগে বাকি সবাইকে এক ঘর ডানে সরিয়ে প্রথম ঘর খালি করতে হয় (ভেতরে একটি System.arraycopy). দশ লাখ জিনিসের তালিকায় প্রতিবার দশ লাখ সরানো — O(n). একইভাবে remove(0) সবাইকে এক ঘর বাঁয়ে টানে। মাঝখানে ঢোকানো বা বাদেও গড়ে অর্ধেক জিনিস সরে। এখানেই LinkedList-এর গল্প শুরু।

৫ · LinkedList — node-এর শিকল, আর কেন বাস্তবে সে হারে

LinkedList কোনো array রাখে না। প্রতিটি জিনিস থাকে একটি আলাদা ছোট object-এ, যাকে বলে node. প্রতিটি node-এর ভেতরে তিনটি জিনিস: আগের node-এর reference (prev), নিজের মান (item) আর পরের node-এর reference (next). দুই দিকেই হাঁটা যায় বলে এর নাম doubly linked list. তালিকা নিজে শুধু মনে রাখে প্রথম আর শেষ node (first, last) আর size. ধাপে ধাপে দেখুন, শুরুতে একটি জিনিস ঢোকাতে দুটির খরচ কেমন:

ধাপ ১ArrayList · add(0, "A")
index:  0   1   2   3   4
       [B] [C] [D] [E] [ ]
size 4, capacity 5 — ঘর ০ খালি করতে হবে
ধাপ ২সবাই এক ঘর ডানে
       [B] [C] [D] [ ] [E]
       [B] [C] [ ] [D] [E]
       [B] [ ] [C] [D] [E]
       [ ] [B] [C] [D] [E]
৪টি সরানো = size — O(n)
ধাপ ৩এবার লেখা
       [A] [B] [C] [D] [E]
১০ লাখ জিনিস থাকলে প্রতিবার ১০ লাখ সরানো
ধাপ ৪LinkedList · একই কাজ
first                 last
  ↓                     ↓
 [B] ⇄ [C] ⇄ [D] ⇄ [E]
প্রতিটি [ ] আলাদা Node object:
prev | item | next
ধাপ ৫শুধু শিকল জোড়া
new Node [A]
A.next = B;  B.prev = A;  first = A;
 [A] ⇄ [B] ⇄ [C] ⇄ [D] ⇄ [E]
কেউ সরেনি — কয়েকটি reference বদল, O(1)
ধাপ ৬কিন্তু get(3)?
ArrayList : শুরু + 3 × ঘরের মাপ → এক লাফে
LinkedList: first → A → B → C → D
index ধরে পৌঁছাতে হাঁটতেই হয় — O(n)
(কাছের প্রান্ত থেকে হাঁটে: শুরু বা শেষ)
  1. ArrayList-এ শুরুতে ঢোকানো। চারটি জিনিস, একটি খালি ঘর শেষে। নতুন “A”-কে বসাতে হবে index ০-তে, কিন্তু সেখানে B বসে আছে।
  2. পেছন থেকে এক এক করে সরানো। E যায় ঘর ৪-এ, D ঘর ৩-এ, তারপর C, তারপর B. (বাস্তবে System.arraycopy এক টানে পুরো খণ্ডটা সরায়, কিন্তু কাজের পরিমাণ তবু size-এর সমানুপাতিক।)
  3. তারপর লেখা। ঘর ০ খালি, A বসল। চারটির তালিকায় এটা কিছুই নয়; দশ লাখের তালিকায় প্রতিটি add(0, x) দশ লাখ ঘর সরায়। লুপে এমন হাজারবার করলে প্রোগ্রাম হামাগুড়ি দেয়।
  4. LinkedList-এর ছবি। চারটি আলাদা Node object, heap-এর যেখানে-সেখানে ছড়ানো, একে অন্যকে হাত ধরে আছে। first দেখায় B-কে, last দেখায় E-কে।
  5. শুরুতে ঢোকানো = শিকলে একটি আংটা। নতুন node A বানানো হলো, A-র next হলো B, B-র prev হলো A, আর first এখন A. তালিকা দশটির হোক বা দশ লাখের — একই কয়েকটি লেখা। addFirst, addLast, removeFirst, removeLast সবই এমন O(1).
  6. উল্টো দিকের দাম। LinkedList-এ কোনো ঠিকানার সূত্র নেই। get(3) পেতে first থেকে একটি একটি করে হাঁটতে হয়। OpenJDK একটু চালাক: index অর্ধেকের কম হলে শুরু থেকে, নইলে শেষ থেকে হাঁটে — তবু মাঝের জিনিসে n/২ ধাপ, অর্থাৎ O(n). আর মাঝে ঢোকানো O(1) শুধু তখনই, যখন আপনি আগে থেকেই সেই জায়গায় (iterator হাতে) দাঁড়িয়ে আছেন; index দিয়ে add(i, x) দিলে আগে হেঁটে যেতে হয়।

Big-O টেবিল — পাশাপাশি

কাজArrayListLinkedList
get(i), set(i, x)O(1)O(n)
শেষে add(x)amortized O(1)O(1)
শুরুতে add(0, x) / remove(0)O(n)O(1)
মাঝে add(i, x) / remove(i)O(n) — সরানোO(n) — হেঁটে পৌঁছানো
iterator হাতে থাকা অবস্থায় it.remove()O(n)O(1)
contains(o), indexOf(o)O(n)O(n)
প্রতি জিনিসে বাড়তি মেমরিএকটি reference (+ কিছু খালি ঘর)একটি পুরো Node object: header + তিনটি reference

টেবিল দেখে মনে হতে পারে, শুরুতে বা মাঝে বেশি ঢোকানো-বাদ থাকলে LinkedList নিশ্চয়ই জিতবে। বাস্তবের মাপজোখে প্রায়ই উল্টোটা দেখা যায়, আর তার কারণ Big-O-তে লেখা থাকে না: cache locality. আধুনিক CPU মূল মেমরি থেকে একটি একটি byte আনে না; পাশাপাশি একটি পুরো খণ্ড একবারে তার খুব দ্রুত cache-এ তুলে আনে। ArrayList-এর reference-গুলো পাশাপাশি, তাই একটি পড়লেই পরের কয়েকটি বিনা খরচে হাতে চলে আসে, আর System.arraycopy-ও একটানা মেমরি সরাতে অত্যন্ত দক্ষ। LinkedList-এর প্রতিটি node heap-এর আলাদা জায়গায় — প্রতিটি next ধরে লাফ মানে সম্ভবত আরেকবার ধীর মূল মেমরিতে যাওয়া। ফলে “O(n) সরানো” প্রায়ই “O(n) হাঁটা”-র চেয়ে অনেক দ্রুত। বাড়তি Node object garbage collector-এর কাজও বাড়ায়।

মূল ধারণা — কোনটা কখন

ডিফল্ট উত্তর ArrayList. প্রায় সব তালিকার কাজ — শেষে যোগ, index ধরে পড়া, পুরোটা ঘুরে দেখা — ArrayList-এ দ্রুততম। দুই প্রান্তে যোগ-বাদ (queue, stack) দরকার হলে LinkedList নয়, ArrayDeque — এটা পর্ব ৩২-এর বিষয়। LinkedList বেছে নেওয়ার মতো পরিস্থিতি খুব বিরল: এমন কাজ, যেখানে iterator হাতে নিয়ে বিশাল তালিকার মাঝখানে অনবরত ঢোকানো-বাদ হয়, আর মাপজোখ করে প্রমাণ পেয়েছেন যে সেখানেই সময় যাচ্ছে। LinkedList class-টি লিখেছিলেন Joshua Bloch. প্রচলিত গল্পমতে ২০১৫ সালের দিকে এক টুইটে তিনি মজা করে প্রশ্ন করেছিলেন, কেউ কি সত্যিই LinkedList ব্যবহার করে। তাঁর কথার সারমর্ম ছিল: class-টি তিনিই লিখেছেন, অথচ নিজে কখনো ব্যবহার করেন না।

৬ · ঘুরে দেখা — আর ঘোরার মধ্যে বাদ দিলে যা ভাঙে

একটি তালিকার প্রতিটি জিনিস দেখার চারটি উপায় আছে:

for (String item : cart) System.out.println(item);           // ১. for-each — সবচেয়ে প্রচলিত

for (int i = 0; i < cart.size(); i++)                        // ২. index লুপ — index-ও দরকার হলে
    System.out.println((i + 1) + ". " + cart.get(i));

Iterator<String> it = cart.iterator();                       // ৩. Iterator — ঘুরতে ঘুরতে বাদ দিতে
while (it.hasNext()) System.out.println(it.next());

cart.forEach(item -> System.out.println(item));              // ৪. forEach + lambda (পর্ব ৩৪)

প্রথমটি আসলে তৃতীয়টির ছদ্মবেশ। Compiler for (String item : cart)-কে বদলে লেখে একটি Iterator দিয়ে: hasNext() জিজ্ঞেস করে আর কিছু আছে কি না, next() পরেরটি দেয়। চতুর্থটিতে item -> ... হলো একটি lambda — “প্রতিটি item নিয়ে এই কাজ করো”; পর্ব ৩৪-এ বিস্তারিত। LinkedList-এ index লুপ (উপায় ২) সাবধানে: প্রতিটি get(i) নিজেই O(n), তাই পুরো লুপ O(n²) — দশ হাজার জিনিসে পাঁচ কোটির মতো ধাপ। for-each বা iterator সব List-এ O(n).

ফেল করা ছাত্রদের বাদ দেওয়া — সবচেয়ে স্বাভাবিক ভুল

একটি পরীক্ষার নম্বরের তালিকা, যেখান থেকে ৩৩-এর কম পাওয়া নম্বরগুলো বাদ দিতে হবে। সবচেয়ে স্বাভাবিক কোড:

import java.util.*;
class Main {
    public static void main(String[] args) {
        List<Integer> marks = new ArrayList<>(List.of(72, 28, 35, 90));
        for (Integer m : marks) {                 // লাইন ৫
            if (m < 33) marks.remove(m);          // ফেল করা বাদ
        }
        System.out.println(marks);
    }
}
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
	at Main.main(Main.java:5)

Stack trace নিচ থেকে পড়ুন: আপনার লাইন ৫ (for-each-এর মাথা) ডেকেছে ArrayList$Itr.next — সেই লুকানো iterator — আর সে ডেকেছে checkForComodification, যে exception ছুড়েছে। আপনার লাইন ৬-এর remove নিজে ঠিকঠাক কাজ করেছে; সমস্যা ধরা পড়েছে পরের ঘুরে। ভেতরে ঠিক কী ঘটল, দেখুন:

1List<Integer> marks = ...(72, 28, 35, 90);2for (Integer m : marks) {  // লুকানো it3    if (m < 33) marks.remove(m);4}5// ঠিক পথ: it.remove() বা removeIf
তালিকা আর লুকানো iterator
তালিকা
size
cursor
modCount
expectedModCount
  1. লুকানো iterator জন্মায়। for-each শুরু হতেই marks.iterator() ডাকা হয়। ArrayList-এর ভেতরে একটি গণনা আছে, modCount — প্রতিটি কাঠামোগত বদলে (add, remove, clear) এক বাড়ে। Iterator জন্মের সময় এর মান নিজের খাতায় টুকে রাখে: expectedModCount = 0.
  2. প্রথম ঘুর। next() আগে যাচাই করে modCount বদলেছে কি না — বদলায়নি। ৭২ দিল, cursor এখন ১। ৭২ ফেল নয়।
  3. দ্বিতীয় ঘুরে বাদ। ২৮ ফেল, তাই marks.remove(m). এখানে m একটি Integer object, তাই remove(Object) চলে — অংশ ৩-এর ফাঁদে পড়েনি। ৩৫ আর ৯০ এক ঘর বাঁয়ে সরল, size ৩, modCount ১। কিন্তু iterator এসব জানে না; তার cursor এখনো ২।
  4. একটি জিনিস চুপচাপ বাদ পড়ল। hasNext() শুধু দেখে cursor আর size সমান কি না: ২ ≠ ৩, তাই “আরও আছে”। অথচ ৩৫ এখন index ১-এ, যেটা cursor পার হয়ে এসেছে — exception না এলে ৩৫ কখনো পরীক্ষাই হতো না।
  5. ধরা পড়া। next() যাচাই করে: modCount ১, খাতায় ০ — মেলে না। সঙ্গে সঙ্গে ConcurrentModificationException. ভুল ফল নিয়ে চুপচাপ এগোনোর চেয়ে থেমে যাওয়া ভালো — একে বলে fail-fast iterator.
  6. সঠিক পথ। একই কাজ it.remove() দিয়ে করলে iterator নিজেই ArrayList-কে মুছতে বলে, তারপর cursor এক ঘর পিছিয়ে ১ করে আর expectedModCount নতুন করে ১ লেখে। হিসাব মেলে, ৩৫ বাদ পড়ে না। আরও সহজ পথ removeIf — নিচে।

সবসময় exception আসে না — আরও খারাপ। একই লুপ [72, 90, 28, 15]-এ চালালে কোনো exception আসে না, ছাপা হয় [72, 90, 15] — ১৫ ফেল, তবু রয়ে গেল! ২৮ বাদ যাওয়ার পর size ৩, আর cursor-ও ৩, তাই hasNext() বলে “শেষ” — next() আর ডাকাই হয় না, যাচাইও হয় না। Javadoc নিজেই বলে, fail-fast আচরণ শুধু “best-effort”; এর ওপর ভরসা করে প্রোগ্রাম লেখা যায় না, এটা শুধু bug ধরার সাহায্য। Index লুপে সামনের দিকে বাদ দেওয়াও একই রোগে ভোগে: [72, 28, 30, 90] থেকে marks.remove(i) দিয়ে বাদ দিলে ফল [72, 30, 90] — ২৮ বাদ যেতেই ৩০ তার জায়গায় সরে আসে, আর i++ তাকে টপকে যায়। পেছন থেকে লুপ চালালে (i--) ঠিক আসে [72, 90].

নিরাপদ দুটি পথ এক প্রোগ্রামে — record দিয়ে ছাত্রের তালিকা থেকে Iterator-এ বাদ, নম্বরের তালিকা থেকে removeIf দিয়ে বাদ, তারপর একটু পরের অংশের পূর্বাভাস:

import java.util.*;
record Student(String name, int marks) { }
class Main {
    public static void main(String[] args) {
        List<Student> cls = new ArrayList<>(List.of(new Student("Rahim", 72),
            new Student("Karim", 28), new Student("Ayesha", 30), new Student("Mitu", 90)));
        Iterator<Student> it = cls.iterator();
        while (it.hasNext())
            if (it.next().marks() < 33) it.remove();   // নিরাপদ পথ ১
        System.out.println(cls);
        List<Integer> marks = new ArrayList<>(List.of(72, 28, 30, 90, 15));
        marks.removeIf(m -> m < 33);                    // নিরাপদ পথ ২
        List<Integer> view = Collections.unmodifiableList(marks);
        List<Integer> copy = List.copyOf(marks);
        marks.add(55);
        System.out.println(marks + " " + view + " " + copy);
        marks.sort(null);
        System.out.println(marks + " max=" + Collections.max(marks));
    }
}

আউটপুট তিন লাইন: [Student[name=Rahim, marks=72], Student[name=Mitu, marks=90]], [72, 90, 55] [72, 90, 55] [72, 90] আর [55, 72, 90] max=90. পাশাপাশি দুই ফেল করা ছাত্র (Karim, Ayesha) দুজনেই বাদ — it.remove() cursor ঠিক রাখে, তাই কেউ টপকে যায়নি। removeIf(m -> m < 33) একই কাজ এক লাইনে করে; ArrayList-এ এটা ভেতরে একবারের ঘোরায় সব বাদ দেয়, তাই অনেকগুলো বাদ হলে বারবার remove-এর চেয়ে দ্রুতও। এরপর একটি মজার জিনিস: marks-এ ৫৫ যোগ করার পর view-তেও ৫৫ দেখা যাচ্ছে, কিন্তু copy-তে নয় — কেন, তা পরের অংশে। শেষে sort(null) স্বাভাবিক ক্রমে সাজাল, আর Collections.max সবচেয়ে বড়টি দিল।

৭ · অপরিবর্তনীয় তালিকা আর view — কে কপি, কে জানালা

সব তালিকা বদলানোর জন্য নয়। সপ্তাহের দিনের নাম, একটি পরীক্ষার বিষয়ের তালিকা, বা কোনো method-এর ফেরত দেওয়া ফল — এগুলো বাইরের কেউ বদলালে bug. Java-তে এমন তালিকা বানানোর কয়েকটি উপায় আছে, আর এদের পার্থক্য সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ।

List.of — সত্যিকারের অপরিবর্তনীয়

import java.util.*;
class Main {
    public static void main(String[] args) {
        List<String> days = List.of("শনি", "রবি", "সোম");
        System.out.println(days.get(1));
        days.add("মঙ্গল");                   // লাইন ৬
    }
}
রবি
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
	at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
	at Main.main(Main.java:6)

Java 9 থেকে আসা List.of(...) একটি unmodifiable collection দেয়: পড়া চলে, কিন্তু add, remove, set, clear — যেকোনো বদলে UnsupportedOperationException. লক্ষ করুন, এটা compile-এর সময় ধরা পড়ে না; List interface-এ add আছে, তাই compiler আপত্তি করে না — ভুল ধরা পড়ে চলার সময়। আরেকটি নিয়ম: List.of কোনো null নেয় না। List.of("a", null) তৈরির সময়েই NullPointerException দেয়, আর এমনকি days.contains(null) জিজ্ঞেস করলেও NullPointerException. (সাধারণ ArrayList-এ null রাখা যায়: [null], size ১।)

View — নিজের কপি নেই, মূলটার জানালা

কিছু method নতুন তালিকা বানায় না; বিদ্যমান ডেটার ওপর একটি নতুন “চেহারা” দেয়। একে বলে collection view. View-এর ভেতরে নিজের কোনো জিনিস নেই — সে শুধু মূলটার দিকে তাকিয়ে আছে। মূলটা বদলালে view-তে সেই বদল দেখা যায়; কোনো কোনো view দিয়ে মূলটা বদলানোও যায়। চারটি পরিচিত উদাহরণ ধাপে ধাপে:

ধাপ ১Arrays.asList — array-এর জানালা
String[] arr = {"চাল", "ডাল"};
List<String> cart = Arrays.asList(arr);

arr  ──▶ [চাল][ডাল] ◀── cart
একটিই array — cart শুধু তাকে List হিসেবে দেখায়
ধাপ ২view দিয়ে লেখা
cart.set(0, "আটা");
System.out.println(arr[0]);   → আটা
set চলে, আর array-ও বদলে যায়
ধাপ ৩কিন্তু মাপ বদলানো যায় না
cart.add("তেল");
UnsupportedOperationException
array-এর মাপ স্থির, তাই add/remove নিষেধ
বদলাতে চাইলে: new ArrayList<>(Arrays.asList(arr))
ধাপ ৪unmodifiableList বনাম copyOf
marks = [72, 90]
view = Collections.unmodifiableList(marks);
copy = List.copyOf(marks);
marks.add(55);
view → [72, 90, 55]   জানালা — বদল দেখে
copy → [72, 90]       কপি — আলাদা
  1. Array-এর ওপর List-এর মুখোশ। Arrays.asList কোনো কপি করে না। ফেরত দেওয়া List ভেতরে আপনার arr-কেই রাখে। তাই এটা দ্রুত, কিন্তু দুটি নাম একই ডেটার — পর্ব ১৩-এর aliasing.
  2. লেখা দুই দিকেই যায়। cart.set(0, "আটা") আসলে array-এর ঘর ০-এ লিখল, তাই arr[0] ছাপলে “আটা”. উল্টোটাও সত্য: arr[1] = ... লিখলে cart.get(1) বদলে যায়।
  3. মাপ স্থির — সবচেয়ে চেনা ফাঁদ। নিচে তার যাচাই করা stack trace দেখুন। নাম “asList” শুনে মনে হয় একটি সাধারণ তালিকা পেলাম, কিন্তু এটা নির্দিষ্ট মাপের। বদলানো যায় এমন তালিকা চাইলে কপি বানান: new ArrayList<>(Arrays.asList(arr)), অথবা শুরু থেকেই new ArrayList<>(List.of(...)).
  4. পাহারাদার জানালা বনাম ছবি তোলা। Collections.unmodifiableList(marks) একটি পড়ার-অনুমতিওয়ালা জানালা: view দিয়ে add দিলে UnsupportedOperationException, কিন্তু যার হাতে মূল marks আছে, সে বদলাতে পারে — আর view সেই বদল দেখায়। List.copyOf(marks) হলো সেই মুহূর্তের ছবি: একটি নতুন, সত্যিকারের অপরিবর্তনীয় কপি (Java 10 থেকে)। পরে মূলটা যা-ই হোক, কপি বদলায় না।
আটা
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.AbstractList.add(AbstractList.java:155)
	at java.base/java.util.AbstractList.add(AbstractList.java:113)
	at Main.main(Main.java:8)

ওপরের trace এসেছে ধাপ ১–৩-এর কোড পুরো প্রোগ্রাম হিসেবে চালিয়ে (লাইন ৮-এ cart.add("তেল"))। লক্ষ করুন, এবার exception আসছে AbstractList.add থেকে — Arrays.asList-এর তালিকা add-কে override-ই করেনি, তাই মূল class-এর “আমি পারি না” সংস্করণটা চলেছে।

subList — তালিকার একটি টুকরোর জানালা

list.subList(from, to) index from থেকে to-এর আগ পর্যন্ত অংশের একটি view দেয়। কপি নয়, তাই খুব কাজের কৌশল চলে: [1, 2, 3, 4, 5, 6]-এ s.subList(1, 4).clear() মাঝের তিনটি মুছে মূল তালিকাকে করে [1, 5, 6]. কিন্তু উল্টো দিকে সাবধান: subList বানানোর পর মূল তালিকায় add বা remove করলে, পরে subList ছুঁতে গেলেই ConcurrentModificationException — একই modCount পাহারা। subList দীর্ঘদিন রেখে দেবেন না; দরকার হলে new ArrayList<>(list.subList(a, b)) দিয়ে কপি করুন।

Array ↔ List রূপান্তর

  • Array থেকে বদলানো যায় এমন List: new ArrayList<>(Arrays.asList(arr)). শুধু পড়ার জন্য: List.of(arr) (কপি) বা Arrays.asList(arr) (view).
  • List থেকে array: String[] back = list.toArray(new String[0]); — ঠিক টাইপের নতুন array. শুধু toArray() দিলে আসে Object[].
  • Primitive array-এর ফাঁদ: int[] a = {1, 2, 3};-এ Arrays.asList(a) তিন জিনিসের তালিকা দেয় না — দেয় List<int[]>, size ১! Generics-এ int চলে না, তাই পুরো array-টাই একটি জিনিস হিসেবে ঢোকে। int[] থেকে List<Integer> বানাতে Stream লাগে (পর্ব ৩৫) বা একটি সাধারণ লুপ।

৮ · সাজানো, Collections-এর কাজের method আর হিসাব খাতার নতুন রূপ

সাজানো — একটি পূর্বাভাস

দুটি সমান উপায়: list.sort(null) আর Collections.sort(list). null মানে “আলাদা কোনো নিয়ম নেই, জিনিসের নিজের স্বাভাবিক ক্রম” — সংখ্যা ছোট থেকে বড়, String বর্ণানুক্রমে (পর্ব ২৩-এর compareTo, যাকে বলে natural ordering). ["Mitu", "Rahim", "Ayesha"] সাজালে [Ayesha, Mitu, Rahim]; Collections.sort(names, Collections.reverseOrder()) দিলে উল্টো: [Rahim, Mitu, Ayesha]. নিজের নিয়মে — GPA অনুযায়ী, তারপর নাম অনুযায়ী — সাজাতে লাগে Comparator, যার পুরো গল্প পর্ব ৩৩-এ। List.of-এর তালিকা সাজাতে গেলে আবার UnsupportedOperationException — সাজানোও একটি বদল।

java.util.Collections — কাজের method-এর বাক্স

নামের শেষে “s” — Collections class আর Collection interface এক জিনিস নয়। পর্ব ১৩-এর Arrays-এর মতো, এটি static method-এ ভরা একটি utility class. সবচেয়ে কাজের কয়েকটি (সব আউটপুট যাচাই করা):

ডাকফলকোথায় কাজে লাগে
Collections.reverse(list)[a, b, c] → [c, b, a]নতুন থেকে পুরোনো লেনদেন
Collections.shuffle(list, new Random(42))[1..8] → [3, 7, 4, 2, 5, 1, 8, 6]কুইজের প্রশ্ন এলোমেলো করা; seed দিলে প্রতিবার একই ক্রম — টেস্টে দরকারি
Collections.max(list), min(list)[72, 28, 90] → ৯০, ২৮সর্বোচ্চ নম্বর, সর্বনিম্ন দাম
Collections.frequency(list, x)[চা, কফি, চা, লাচ্ছি, চা]-এ “চা” → ৩চায়ের দোকানে কোন অর্ডার কতবার
Collections.unmodifiableList(list)পড়ার-অনুমতিওয়ালা viewclass-এর ভেতরের তালিকা বাইরে দেখানো

shuffle-এর ক্রম Random-এর বাস্তবায়নের ওপর নির্ভর করে; seed ছাড়া চালালে প্রতিবার আলাদা ফল আসবে, আর সেটাই স্বাভাবিক।

List<int> নেই — আর boxing-এর দাম

পর্ব ২৮-এ দেখেছেন, type argument শুধু reference টাইপ হতে পারে। চেষ্টা করলে:

Main.java:4: error: unexpected type
        List<int> marks = new ArrayList<>();
             ^
  required: reference
  found:    int
1 error

তাই List<Integer>, আর marks.add(72)-এ autoboxing নিজে থেকে Integer object বানায়। এর মানে, ArrayList-এর ভেতরের array-তে সংখ্যা নয়, reference থাকে — প্রতিটি reference heap-এর আলাদা একটি Integer object দেখায়। int[]-এর তুলনায় মেমরি কয়েক গুণ, আর প্রতিটি পড়ায় একটি বাড়তি লাফ (আবার সেই cache locality). কয়েকশো বা কয়েক হাজার নম্বরে এটা কোনো ব্যাপার নয়; কয়েক কোটি সংখ্যার হিসাবে int[] বা IntStream ভাবুন। আর Integer-এর মান তুলনায় == নয়, equals — পর্ব ০৬-এর Integer cache-এর ফাঁদ মনে রাখুন।

হিসাব খাতা — array থেকে ArrayList

পর্ব ২২-এর ExpenseBook-এর ভেতরটা এবার বদলে দেখুন। বাইরের কেউ (কমান্ড class, main loop) কিছুই টের পাবে না, কারণ array আর count দুটিই private ছিল:

class ExpenseBook {
    private final List<Expense> items = new ArrayList<>();   // capacity-র চিন্তা শেষ

    boolean add(Expense e) { return items.add(e); }           // আর কখনো "ভরা" নয়
    Expense removeLast() { return items.isEmpty() ? null : items.removeLast(); }
    int size() { return items.size(); }
    Expense get(int i) { return items.get(i); }
    List<Expense> all() { return Collections.unmodifiableList(items); }  // বাইরে শুধু পড়া
    int total(String cat) {
        int sum = 0;
        for (Expense e : items)
            if (cat == null || e.category().equals(cat)) sum += e.amount();
        return sum;
    }
}

তিনটি জিনিস লক্ষ করুন। প্রথমত, count ভ্যারিয়েবল আর “খাতা ভরা” পরীক্ষা পুরো উধাও — ArrayList নিজের size নিজে রাখে। দ্বিতীয়ত, Java 21-এর removeLast() সরাসরি ব্যবহার হয়েছে; পুরোনো items[count] = null লিখে garbage-এর জন্য reference মুছে দেওয়ার কাজটাও ArrayList নিজে করে। তৃতীয়ত, নতুন all() method ভেতরের তালিকা সরাসরি ফেরত দেয় না — দেয় একটি পড়ার-অনুমতিওয়ালা view. ভেতরের items ফেরত দিলে যে কেউ book.all().clear() লিখে পুরো খাতা মুছে ফেলতে পারত, আর encapsulation-এর সব পরিশ্রম জলে যেত। সম্পূর্ণ আলাদা স্থির ছবি চাইলে List.copyOf(items) — পর্ব ১৭-এর defensive copy.

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

C-তে বাড়তে পারা তালিকা নিজেকে বানাতে হয় — malloc/realloc দিয়ে dynamic array, বা হাতে struct আর pointer জুড়ে linked list; ভুল হলে memory leak বা crash. C++-এর std::vector হলো ArrayList-এর ভাই (সেখানে vector<int> সরাসরি চলে, boxing নেই) আর std::list হলো LinkedList-এর; C++ জগতেও পরামর্শ একই — ডিফল্ট vector. Python-এর list আসলে একটি dynamic array, ArrayList-এর মতোই, কিন্তু যেকোনো টাইপ মিশিয়ে রাখা যায়, আর ঘোরার মধ্যে বাদ দিলে Python কোনো exception দেয় না — চুপচাপ জিনিস টপকে যায়। গভীরে দেখুন C-র linked list, C++-এর vector আর Python-এর list পর্ব।

পরীক্ষা ও ইন্টারভিউতে
  • ArrayList আর LinkedList-এর পার্থক্য? — ArrayList ভেতরে dynamic array: get(i) O(1), শুরু বা মাঝে ঢোকানো O(n); LinkedList doubly linked node: দুই প্রান্তে O(1), get(i) O(n). বাস্তবে প্রায় সবসময় ArrayList দ্রুত (cache locality).
  • ArrayList-এর capacity আর size কী? — capacity ভেতরের array-এর ঘর, size ভরা ঘর; ভরে গেলে OpenJDK প্রায় ১.৫ গুণ বড় array বানিয়ে কপি করে, ডিফল্ট শুরু ১০।
  • ConcurrentModificationException কখন আসে? — iterator (বা for-each) চলার সময় তালিকাটি অন্য পথে কাঠামোগতভাবে বদলালে; সমাধান Iterator.remove() বা removeIf.
  • Arrays.asList আর List.of-এর পার্থক্য? — asList নির্দিষ্ট মাপের, array-এর view, set চলে, null চলে; List.of পুরো অপরিবর্তনীয়, কোনো বদল বা null চলে না।
  • List<Integer>-এ remove(1) কী মুছে? — index 1-এর জিনিস, কারণ remove(int) boxing ছাড়াই মেলে; মান মুছতে remove(Integer.valueOf(1)).

৯ · AI-এর যুগে — তালিকার কোড রিভিউ করতে শিখুন

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

AI যা করেকেন সমস্যাআপনি যা করবেন
“ঢোকানো-বাদ বেশি” শুনেই LinkedListindex ধরে পড়লে O(n), cache-এ ধীর, মেমরি বেশিজিজ্ঞেস করুন: কোথায় ঢোকানো, index ধরে পড়া আছে কি? প্রায় সবসময় ArrayList; queue হলে ArrayDeque
for-each-এর ভেতরে list.remove(x)ConcurrentModificationException, বা আরও খারাপ — চুপচাপ জিনিস টপকে যাওয়াremoveIf বা Iterator.remove(); সামনের দিকের index লুপে remove-ও নয়
Arrays.asList(...) বানিয়ে পরে addচলার সময় UnsupportedOperationExceptionবদলানো যায় এমন তালিকা চাইলে new ArrayList<>(...)-এ মোড়ান
getter থেকে ভেতরের List সরাসরি ফেরতবাইরের কোড ভেতরের অবস্থা বদলে দিতে পারেList.copyOf বা Collections.unmodifiableList ফেরত দিন
List<Integer>-এ মান ধরে বাদ দিতে remove(id), যেখানে id একটি intindex ধরে মুছে যায় — ভুল জিনিস, কোনো error নেইremove(Integer.valueOf(id)); রিভিউতে প্রতিটি remove-এর argument-এর টাইপ দেখুন
LinkedList-এ for (int i…) list.get(i)লুকানো O(n²)for-each বা iterator
বাঁয়ে ArrayList<String>, parameter-এ ArrayListimplementation-এ বাঁধা; List.of দেওয়া যায় নাভ্যারিয়েবল, parameter, return সবখানে List

তালিকা নিয়ে কোড লেখাতে বা রিভিউ করাতে এই prompt ব্যবহার করুন — শর্তগুলো AI-কে প্রতিটি সিদ্ধান্তের কারণ বলতে বাধ্য করে:

আমি Java 21 শিখছি; List, ArrayList, LinkedList, Iterator আর generics জানি। কাজ: [যেমন — একটি ক্লাসের উপস্থিতির তালিকা থেকে অনুপস্থিতদের বাদ দিয়ে, বাকিদের নাম অনুযায়ী সাজিয়ে ফেরত দেওয়া] শর্ত: ১. ভ্যারিয়েবল, parameter আর return টাইপে List লিখবে, ArrayList বা LinkedList নয়। ২. LinkedList ব্যবহার করলে Big-O আর cache locality দিয়ে কারণ দেখাবে; নইলে ArrayList. ৩. ঘোরার মধ্যে কিছু বাদ দিতে হলে removeIf বা Iterator.remove() — for-each-এর ভেতরে remove নয়। ৪. Arrays.asList-এর ফলে add/remove করবে না; বদলানো যায় এমন তালিকা চাইলে new ArrayList<>(...) দিয়ে মুড়বে। ৫. কোনো class-এর ভেতরের তালিকা বাইরে দিলে List.copyOf বা Collections.unmodifiableList দেবে। ৬. List<Integer>-এ remove ডাকলে বলবে সেটা index ধরে না মান ধরে। ৭. শেষে প্রতিটি মূল কাজের সময়-জটিলতা (Big-O) একটি ছোট টেবিলে দাও।

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

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

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

প্র ০১Java ১৯৯৬ সালে Vector আর Hashtable নিয়ে এসেছিল। তাহলে ১৯৯৮ সালে আবার পুরো একটি Collections Framework কেন বানাতে হলো?

JDK 1.0-এ ডেটা রাখার জন্য ছিল মাত্র কয়েকটি class: Vector (বাড়তে পারা তালিকা), Hashtable (key-value), Stack (Vector-এর subclass) আর ঘুরে দেখার জন্য Enumeration. এগুলো কাজ চালাত, কিন্তু কোনো সাধারণ নকশা ছিল না। Vector আর Hashtable-এর method-এর নাম আলাদা ধাঁচের, কোনো সাধারণ interface নেই, তাই “যেকোনো তালিকা নেয়” এমন method লেখা যেত না। আর প্রায় প্রতিটি method synchronized — একসাথে একাধিক thread থেকে ব্যবহার নিরাপদ করার জন্য। একটি thread-এর সাধারণ প্রোগ্রামেও সেই lock-এর খরচ দিতে হতো। Stack extends Vector হওয়ায় একটি stack-এর মাঝখানে insertElementAt দিয়ে জিনিস ঢোকানো যেত — inheritance-এর অপব্যবহারের ক্লাসিক উদাহরণ (পর্ব ১৯)।

Bloch-এর নকশা। Sun-এ Joshua Bloch এর সমাধান হিসেবে Collections Framework নকশা করেন, যা আসে JDK 1.2-তে, ১৯৯৮ সালের ডিসেম্বরে। মূল ধারণা ছিল অংশ ২-এর মানচিত্র: অল্প কয়েকটি মূল interface (Collection, List, Set, Map, আর তাদের Sorted সংস্করণ), প্রতিটির একাধিক implementation, আর সবার জন্য একসাথে কাজ করা algorithm (Collections.sort, binarySearch). Doug Lea-র আগের collections library-র প্রভাবও স্বীকৃত। সচেতন সিদ্ধান্তগুলো ছিল: নতুন class-গুলো synchronized নয় (দরকার হলে Collections.synchronizedList দিয়ে মোড়ানো যায়), iterator fail-fast, আর ঐচ্ছিক method — যে implementation বদল সমর্থন করে না, সে UnsupportedOperationException দেবে। শেষ সিদ্ধান্তটি নিয়ে আজও বিতর্ক হয়, কারণ এতে “বদলানো যায় কি না” টাইপ দেখে বোঝা যায় না; কিন্তু এর ফলে interface-এর সংখ্যা অল্প থাকে।

পুরোনোদের কী হলো। Vector আর Hashtable মুছে ফেলা হয়নি — Java পুরোনো কোড ভাঙে না। বরং তাদের পেছন থেকে নতুন interface-এ জুড়ে দেওয়া হয়: Vector এখন একটি List, Hashtable একটি Map. তাই আজও কিছু পুরোনো API (যেমন Swing-এর কিছু অংশ) Vector ফেরত দেয়। নতুন কোডে এগুলোর জায়গা নিয়েছে ArrayList আর HashMap; একাধিক thread-এর জন্য পরে Java 5-এ (২০০৪) Doug Lea-র নেতৃত্বে আসে java.util.concurrent, যা এক বড় lock-এর চেয়ে অনেক সূক্ষ্ম কৌশল ব্যবহার করে। Bloch পরে Effective Java বইয়ে API নকশার এই অভিজ্ঞতাগুলো লিখেছেন।

মূল উপলব্ধি: ভালো library-র শক্তি তার class-এর সংখ্যায় নয়, তার interface-এর নকশায় — Collections Framework-এর অল্প কয়েকটি interface-ই পঁচিশ বছরের বেশি সময় ধরে Java-র প্রায় সব কোডের সাধারণ ভাষা হয়ে আছে।

প্র ০২ArrayList ভরে গেলে দেড় গুণ বাড়ে, অথচ অনেক জায়গায় dynamic array দ্বিগুণ হয়। ১.৫ নাকি ২ — এই সংখ্যার পেছনে যুক্তি কী?

যেকোনো গুণক (১-এর বেশি) দিলেই অংশ ৪-এর amortized O(1) পাওয়া যায়, তাই প্রশ্নটা গতির চেয়ে বেশি মেমরির। দুটি বিপরীত টান কাজ করে। গুণক বড় হলে কপি কম হয়: দ্বিগুণে প্রতি জিনিসে গড়ে মোটামুটি একটি কপি, দেড় গুণে প্রায় দুটি। কিন্তু গুণক বড় হলে খালি ঘরও বেশি পড়ে থাকে: ঠিক বড় হওয়ার পরমুহূর্তে দ্বিগুণে প্রায় অর্ধেক array খালি, দেড় গুণে এক-তৃতীয়াংশ।

পুরোনো মেমরি আবার ব্যবহারের যুক্তি। C++ জগতে একটি সুন্দর যুক্তি প্রচলিত, যা Facebook-এর folly library-র FBVector-এর নথিতে বিখ্যাত হয়। ধরুন allocator মুক্ত করা মেমরি খণ্ডগুলো পাশাপাশি জুড়ে আবার ব্যবহার করতে পারে। দ্বিগুণ করলে মাপগুলো ১, ২, ৪, ৮… — আর আগের সব খণ্ডের যোগফল (১ + ২ + ৪ = ৭) সবসময় পরের দরকারের (৮) চেয়ে ছোট। অর্থাৎ পুরোনো মুক্ত জায়গা কখনোই নতুন array-এর জন্য যথেষ্ট হয় না; array প্রতিবার নতুন জায়গায় সরে যায়। গুণক সোনালি অনুপাতের (প্রায় ১.৬১৮) কম হলে কয়েক ধাপ পরে আগের খণ্ডগুলোর যোগফল নতুন দরকারকে ছাড়িয়ে যায়, তাই পুরোনো জায়গা আবার কাজে লাগতে পারে। ১.৫ এই সীমার নিচে, আর হিসাবেও সস্তা — old + (old >> 1), একটি shift আর একটি যোগ। বাস্তবে C++-এর বাস্তবায়নগুলো ভিন্ন পথ নিয়েছে: GCC-র libstdc++ দ্বিগুণ করে, Microsoft-এর বাস্তবায়ন দেড় গুণ।

Java-তে যুক্তিটা একটু অন্যরকম। JVM-এর garbage collector বেশিরভাগ ক্ষেত্রে জীবিত object-গুলোকে সরিয়ে মেমরি ঘন করে (compaction), তাই “পাশাপাশি মুক্ত খণ্ড জোড়া লাগানো”-র যুক্তি C++-এর মতো সরাসরি খাটে না। Java-তে দেড় গুণের মূল লাভ হলো কম খালি জায়গা — লাখ লাখ ছোট তালিকা যেখানে একসাথে বেঁচে থাকে, সেখানে এটা গুরুত্বপূর্ণ। মজার তথ্য: পুরোনো Vector ডিফল্টে দ্বিগুণ হয় (যদি না আপনি নিজে বৃদ্ধির ধাপ বলে দেন), আর Python-এর list আরও কম, প্রায় এক-অষ্টমাংশ করে বাড়ে। আর একেবারে প্রথম array তৈরি পিছিয়ে দেওয়া (অংশ ৪-এর ধাপ ১) — Java 7-এর একটি আপডেটে আসা এই ছোট বদলও একই মেমরি-সচেতনতার ফল।

মূল উপলব্ধি: বৃদ্ধির গুণক হলো গতি আর মেমরির মধ্যে একটি আপস; ১.৫ বা ২ কোনোটাই “সঠিক” নয় — নির্ভর করে memory allocator, garbage collector আর আপনার তালিকার সংখ্যা ও আকারের ওপর।

প্র ০৩Big-O বলে LinkedList-এ মাঝে ঢোকানো সস্তা, তবু মাপজোখে ArrayList জেতে। আধুনিক CPU-তে linked list এত ধীর কেন?

Big-O গোনে ধাপের সংখ্যা, প্রতিটি ধাপের দাম নয়। আর আধুনিক কম্পিউটারে সব ধাপের দাম এক নয়। CPU আর মূল মেমরির (RAM) গতির ফারাক কয়েক দশক ধরে বেড়েছে: CPU-র ভেতরের ছোট cache থেকে একটি মান পড়তে লাগে কয়েকটি cycle, আর RAM থেকে আনতে প্রায় একশো ন্যানোসেকেন্ডের কাছাকাছি — কয়েকশো cycle. এই ফাঁক ঢাকতে CPU-তে কয়েক স্তরের cache থাকে (L1, L2, L3), আর মেমরি আসে খণ্ডে — সাধারণত ৬৪ byte-এর “cache line”. তার ওপর CPU-র prefetcher লক্ষ করে আপনি পরপর ঠিকানা পড়ছেন কি না; পড়লে পরের খণ্ড আগেভাগেই আনিয়ে রাখে।

ArrayList এই যন্ত্রের বন্ধু। তার reference-গুলো পাশাপাশি: একটি cache line-এ অনেকগুলো, আর prefetcher পুরো সারিটা আগেভাগে টেনে আনে। মাঝে ঢোকানোর জন্য যে “সরানো”, সেটাও একটানা মেমরি কপি — CPU-র সবচেয়ে দক্ষ কাজগুলোর একটি। LinkedList-এর প্রতিটি node heap-এর কোথায় আছে তা নির্ভর করে কখন তৈরি হয়েছিল তার ওপর; next ধরে প্রতিটি লাফ পরের ঠিকানা না জেনে করা যায় না, তাই prefetcher অসহায়, আর প্রায় প্রতিটি লাফে cache miss-এর সম্ভাবনা। Java-তে আরও একটি স্তর আছে: প্রতিটি node একটি পূর্ণ object (header-সহ), তার ভেতরের item আরেকটি object-এর reference — অর্থাৎ একটি মান পড়তে দুটি লাফ।

Stroustrup-এর পরীক্ষা। C++-এর স্রষ্টা Bjarne Stroustrup ২০১২ সালের GoingNative সম্মেলনের বক্তৃতায় একটি পরীক্ষার কথা বলেন: এলোমেলো সংখ্যাগুলো একটি সাজানো ক্রম বজায় রেখে একে একে ঢোকানো, তারপর এলোমেলো অবস্থান থেকে একে একে বাদ দেওয়া। Big-O অনুযায়ী linked list-এর ঢোকানো O(1) হওয়ার কথা, কিন্তু সঠিক জায়গা খুঁজে পেতে দুটিকেই সারি ধরে হাঁটতে হয় — আর সেই হাঁটায় vector এত দ্রুত যে বিশাল আকারেও vector অনেক গুণ এগিয়ে থাকে। তাঁর উপসংহার: ডিফল্ট হিসেবে vector ব্যবহার করুন, আর ডেটা ঘন ও পাশাপাশি রাখুন।

শিক্ষা Big-O ফেলে দেওয়া নয়। বিশাল n-এ O(n) আর O(n²)-এর পার্থক্য কোনো cache বাঁচাতে পারে না। কিন্তু একই Big-O শ্রেণির মধ্যে ধ্রুবক গুণক — memory access-এর ধরন — দশ গুণ বা তারও বেশি পার্থক্য আনতে পারে। তাই পারফরম্যান্সের প্রশ্নে উত্তর আসে মাপজোখ থেকে (Java-তে JMH-এর মতো টুল দিয়ে), অনুমান থেকে নয়।

মূল উপলব্ধি: আধুনিক হার্ডওয়্যারে “কতগুলো ধাপ” যতটা গুরুত্বপূর্ণ, “ডেটা মেমরিতে কোথায়” ঠিক ততটাই; পাশাপাশি থাকা ডেটা প্রায় সবসময় জেতে।

প্র ০৪Java 9-এর List.of কেন null নেয় না, যেখানে ArrayList নেয়? এমনকি contains(null)-এও exception কেন?

Java 9-এর আগে একটি ছোট অপরিবর্তনীয় তালিকা বানাতে লিখতে হতো Collections.unmodifiableList(new ArrayList<>(Arrays.asList("a", "b"))) — তিন স্তরের মোড়ক, আর ফলটা আসলে একটি view, সত্যিকারের অপরিবর্তনীয় নয়। Google-এর Guava library ততদিনে ImmutableList.of(...) দিয়ে দেখিয়ে দিয়েছিল যে আরও ভালো পথ আছে। Java 9 (২০১৭)-এ JEP 269 — “Convenience Factory Methods for Collections”, যার নেতৃত্বে ছিলেন Oracle-এর Stuart Marks — নিয়ে আসে List.of, Set.of, Map.of. পরে Java 10-এ আসে List.copyOf.

Null বাদ দেওয়ার যুক্তি। প্রথমত, collection-এ null প্রায় সবসময় bug-এর চিহ্ন — কোনো ডেটা ভুল করে খালি রয়ে গেছে। তৈরির মুহূর্তেই NullPointerException দিলে ভুলটা ধরা পড়ে উৎসের কাছে, অনেক পরে অন্য কোনো জায়গায় নয় — fail-fast নীতি। দ্বিতীয়ত, null অর্থের দ্ব্যর্থতা আনে: Map.get null দিলে মানে কি “key নেই” নাকি “key আছে, মান null”? Guava-র দল তাদের নিজেদের কোডবেস পর্যালোচনা করে জানিয়েছিল যে collection-এ null রাখার বৈধ প্রয়োজন খুব কমই আসে। তৃতীয়ত, null না থাকলে ভেতরের বাস্তবায়ন সরল ও দ্রুত হয় — প্রতিটি তুলনায় আলাদা null-পরীক্ষা লাগে না, আর ছোট আকারের জন্য বিশেষ class (একটি বা দুটি জিনিসের তালিকা) বানানো সহজ হয়।

contains(null) কেন ছোড়ে। এটা নকশাকারদের সচেতন বেছে নেওয়া পথ। Collection interface-এর Javadoc আগে থেকেই অনুমতি দেয় যে null-নিষিদ্ধ collection, null দিয়ে খোঁজার প্রশ্নেও NullPointerException দিতে পারে। যুক্তি হলো, যে কোড null দিয়ে খুঁজছে, সে সম্ভবত নিজেই ভুল অবস্থায় আছে — চুপচাপ false ফেরত দিলে সেই bug লুকিয়ে থাকত। দাম হলো, একটি সাধারণ ArrayList-কে List.of-এ বদলালে পুরোনো কোড চলার সময় ভাঙতে পারে।

আরও দুটি সচেতন সিদ্ধান্ত। Set.of-এ একই জিনিস দুবার দিলে IllegalArgumentException — তৈরির সময় ভুল ধরা। আর Set.of, Map.of-এর ঘোরার ক্রম কোনো নিয়মে বাঁধা নয়; OpenJDK-র বাস্তবায়নে সেটা ইচ্ছা করেই প্রতিবার JVM চালুর সময় এলোমেলো হয়, যাতে কেউ কোনো নির্দিষ্ট ক্রমের ওপর ভরসা করে কোড না লেখে (পর্ব ৩১-এ আবার দেখবেন)।

মূল উপলব্ধি: নতুন API-গুলো পুরোনো সুবিধাগুলো (null, নমনীয়তা) ইচ্ছা করে কেড়ে নিয়েছে, যাতে ভুল দ্রুত ও উৎসের কাছে ধরা পড়ে — নিরাপত্তার বিনিময়ে একটু কঠোরতা।

প্র ০৫“Fail-fast” iterator আর তথাকথিত “fail-safe” iterator-এর পার্থক্য কী? একাধিক thread একই তালিকা পড়লে-লিখলে কী হয়?

ArrayList, LinkedList, HashMap-এর iterator হলো fail-fast: অংশ ৬-এর modCount পরীক্ষা করে, আর অপ্রত্যাশিত বদল দেখলে ConcurrentModificationException ছোড়ে। নামটা বিভ্রান্তিকর — “concurrent” শুনে মনে হয় একাধিক thread, অথচ আমাদের উদাহরণে thread একটিই; সমস্যা ছিল একই thread-এর দুটি পথ (iterator আর list.remove) দিয়ে বদল। আর Javadoc স্পষ্ট বলে, এই পরীক্ষা “best-effort”: একাধিক thread-এর ক্ষেত্রে, synchronization ছাড়া, এটা ঠিকমতো ধরা পড়ার কোনো নিশ্চয়তা নেই। অংশ ৬-এর [72, 90, 15] উদাহরণে দেখেছেন, এক thread-এও ধরা না পড়তে পারে।

একাধিক thread-এ সাধারণ ArrayList. ArrayList thread-safe নয়। দুটি thread একসাথে add করলে দুজনেই হয়তো একই size পড়ে একই ঘরে লেখে — একটি জিনিস হারিয়ে যায়; বা বড় হওয়ার মাঝখানে অন্যজন পুরোনো array-তে লেখে। ফল: হারানো ডেটা, null ঘর, কখনো ArrayIndexOutOfBoundsException — আর প্রতিবার আলাদা, তাই খুঁজে পাওয়া কঠিন। পর্ব ৪০-এ thread আর synchronization নিয়ে বিস্তারিত।

“Fail-safe” — আসলে snapshot বা weakly consistent. Java 5-এর java.util.concurrent প্যাকেজের collection-গুলো অন্য কৌশল নেয়। CopyOnWriteArrayList প্রতিটি লেখার সময় পুরো ভেতরের array কপি করে নতুনটা বসিয়ে দেয়; একটি iterator জন্মের মুহূর্তের array ধরে রাখে — একটি snapshot. তাই ঘোরার মধ্যে অন্য কেউ (বা আপনি নিজে) লিখলেও কোনো exception নেই, তবে iterator সেই নতুন বদল দেখে না, আর iterator দিয়ে remove করা যায় না। লেখা ব্যয়বহুল (প্রতিবার O(n) কপি), তাই এটা উপযুক্ত যেখানে পড়া অনেক, লেখা কদাচিৎ — যেমন event listener-এর তালিকা। ConcurrentHashMap-এর iterator “weakly consistent”: কখনো exception দেয় না, ঘোরার সময়ের কিছু বদল দেখতেও পারে, না-ও পারে। “Fail-safe” শব্দটি Javadoc-এর আনুষ্ঠানিক শব্দ নয়, ইন্টারভিউয়ের প্রচলিত নাম — Javadoc বলে “snapshot” আর “weakly consistent”.

মূল উপলব্ধি: Fail-fast iterator একটি bug-ধরার যন্ত্র, thread-নিরাপত্তার ব্যবস্থা নয়; একাধিক thread-এর জন্য বানানো collection ভুল ধরার বদলে ভিন্ন চুক্তি দেয় — পুরোনো ছবি বা আংশিক হালনাগাদ — আর তার দাম দেয় লেখার খরচে।

অনুশীলন

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

  1. আউটপুট বলুন, তারপর ঠিক করুন: List<Integer> marks = new ArrayList<>(List.of(72, 28, 30, 90)); তারপর for (int i = 0; i < marks.size(); i++) if (marks.get(i) < 33) marks.remove(i); — কী ছাপা হবে? কোনো exception আসবে কি? দুটি আলাদা উপায়ে ঠিক করুন।

    ছাপা হয় [72, 30, 90], কোনো exception নেই — index লুপে iterator নেই, তাই modCount-এর পাহারাও নেই। i = ১-এ ২৮ বাদ গেল, ৩০ সরে এল index ১-এ, কিন্তু i++ তাকে টপকে i = ২-এ (৯০) চলে গেল। এখানে remove(i)-এ i একটি int, তাই index ধরে মোছে — যা এখানে উদ্দেশ্যও। ঠিক করার উপায়: (১) পেছন থেকে লুপ — for (int i = marks.size() - 1; i >= 0; i--); বাদ দিলে শুধু পেছনের (আগেই দেখা) জিনিসগুলো সরে, ফল [72, 90]. (২) সবচেয়ে পরিষ্কার: marks.removeIf(m -> m < 33);. বাদ দেওয়ার পর i-- লেখাও কাজ করে, কিন্তু পড়তে বিভ্রান্তিকর।

  2. কপি গুনুন: new ArrayList<>()-এ একে একে ৫০টি জিনিস যোগ করলে ভেতরের array কতবার বড় হবে, আর মোট কয়টি reference কপি হবে? (নতুন capacity = পুরোনো + পুরোনো/২, পূর্ণসংখ্যায়।) new ArrayList<>(50) দিলে কী বদলায়?

    প্রথম add-এ ১০ ঘরের array (কপি নেই)। তারপর বড় হয় ১১তম add-এ (১০ → ১৫, ১০টি কপি), ১৬তম-এ (১৫ → ২২, ১৫টি), ২৩তম-এ (২২ → ৩৩, ২২টি), ৩৪তম-এ (৩৩ → ৪৯, ৩৩টি) আর ৫০তম-এ (৪৯ → ৭৩, ৪৯টি)। মোট পাঁচবার বড় হওয়া, কপি ১০ + ১৫ + ২২ + ৩৩ + ৪৯ = ১২৯টি — ৫০টি জিনিসের জন্য প্রায় আড়াই গুণ, আর শেষে ৭৩ ঘরের মধ্যে ২৩টি খালি। new ArrayList<>(50) দিলে শুরুতেই ৫০ ঘর, তাই একটিও কপি নেই, খালি ঘরও নেই। তবে মনে রাখুন, এটা শুধু capacity — size() তখনো ০, আর get(0) দিলে IndexOutOfBoundsException.

  3. হিসাব খাতায় নতুন কমান্ড: অংশ ৮-এর ArrayList-ভিত্তিক ExpenseBook-এ একটি method লিখুন int removeCategory(String cat), যা ওই খাতের সব খরচ মুছে দেয় আর কয়টি মুছল তা ফেরত দেয়। আগের array-with-count সংস্করণে এই কাজ কতটা কঠিন হতো?
    int removeCategory(String cat) {
        int before = items.size();
        items.removeIf(e -> e.category().equals(cat));
        return before - items.size();
    }

    removeIf ঘোরার মধ্যে নিরাপদে মোছে, আর ArrayList-এ সব বাদ একবারের ঘোরায় হয় — প্রতিটির জন্য আলাদা সরানো নয়। array-with-count সংস্করণে আপনাকে হাতে দুটি index (পড়ার আর লেখার) চালিয়ে বাকি খরচগুলো সামনে টেনে আনতে হতো, শেষে count কমিয়ে খালি হওয়া ঘরগুলোতে null লিখতে হতো — সহজেই off-by-one ভুল হয়। (মজার তথ্য: ArrayList-এর removeIf ভেতরে ঠিক এই দুই-index কৌশলটাই করে।)

  4. সঠিক কাঠামো বাছুন: প্রতিটির জন্য ArrayList, LinkedList, int[], List.of নাকি অন্য কিছু? (ক) সারা দিনের দোকানের বিক্রির লগ — শেষে যোগ, পরে index ধরে দেখা আর পুরোটা ঘুরে হিসাব। (খ) একটি সেন্সর থেকে ঠিক ১ কোটি তাপমাত্রার রিডিং, শুধু গড় বের করা। (গ) প্রিন্টারের কাজের লাইন — এক প্রান্তে ঢোকে, অন্য প্রান্ত থেকে বের হয়। (ঘ) সপ্তাহের সাত দিনের নাম।

    (ক) ArrayList — শেষে যোগ amortized O(1), index ধরে পড়া O(1), ঘোরা cache-বান্ধব; এটাই ডিফল্ট। (খ) double[] — সংখ্যা নির্দিষ্ট, আর ১ কোটি boxed Double object মানে বহু গুণ মেমরি আর garbage collector-এর চাপ; primitive array পাশাপাশি সংখ্যা রাখে। (গ) এটা একটি queue: LinkedList কাজ করবে, কিন্তু ভালো পছন্দ ArrayDeque — পর্ব ৩২-এ দেখবেন কেন। (ঘ) List.of(...) — কখনো বদলাবে না, তাই অপরিবর্তনীয় তালিকা; কেউ ভুল করে বদলাতে চাইলে সঙ্গে সঙ্গে exception. লক্ষ করুন, চারটির কোনোটিতেই LinkedList সেরা উত্তর নয়।

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

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

ক্রমএকটি ভরা ArrayList-এ add(x) ডাকলে ভেতরে যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 add(x) ডাকা হলো
  2. 🔍 ArrayList দেখে size আর capacity সমান — জায়গা নেই
  3. 📐 নতুন capacity হিসাব: পুরোনো + পুরোনোর অর্ধেক
  4. 📋 বড় নতুন array বানিয়ে পুরোনো সব reference কপি
  5. ✍️ x বসে ঘর size-এ, size এক বাড়ে; পুরোনো array garbage

Java-তে List<Integer> rolls = new ArrayList<>(List.of(3, 7, 1)); rolls.remove(1); System.out.println(rolls); — কী ছাপা হবে?

  • [3, 7]
  • [3, 1]
  • [7, 1]
  • compile error: ambiguous call

Java-তে List<String> days = List.of("শনি", "রবি"); days.add("সোম"); — কী ঘটে?

  • তালিকায় "সোম" যোগ হয়, size 3
  • compile error: cannot find symbol add
  • চলার সময় UnsupportedOperationException
  • add চুপচাপ false ফেরত দেয়

দশ লাখ জিনিসের একটি ArrayList-এ নিচের কোন কাজটি সবচেয়ে ধীর (O(n))?

  • add(0, x) — শুরুতে ঢোকানো
  • get(500000)
  • add(x) — শেষে যোগ
  • size()

একটি ArrayList থেকে ৩৩-এর কম সব নম্বর বাদ দিতে হবে। নিচের কোনটি নিরাপদ ও সঠিক?

  • for (Integer m : list) if (m < 33) list.remove(m);
  • for (int i = 0; i < list.size(); i++) if (list.get(i) < 33) list.remove(i);
  • list.forEach(m -> { if (m < 33) list.remove(m); });
  • list.removeIf(m -> m < 33);

Java-তে String[] arr = {"a", "b"}; List<String> v = Arrays.asList(arr); v.set(0, "z"); System.out.println(arr[0]); — কী ছাপা হবে?

  • a
  • z
  • UnsupportedOperationException
  • null

দশ লাখ জিনিসের একটি Java LinkedList-এ get(500000) ডাকলে কী ঘটে?

  • ভেতরের array থেকে এক লাফে পায় — O(1)
  • UnsupportedOperationException, কারণ LinkedList-এ index নেই
  • প্রান্তের node থেকে একে একে হেঁটে পৌঁছায় — O(n)
  • binary search করে O(log n)-এ পায়

শব্দকোষ

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

মনে রাখুন

  1. List = ক্রম, index, নিজে বাড়া: List<String> x = new ArrayList<>(); — বাঁয়ে interface, ডানে implementation; parameter আর return-এও List.
  2. ArrayList ভেতরে array: capacity ভরে গেলে প্রায় ১.৫ গুণ বড় array-এ কপি; শেষে যোগ amortized O(1), get O(1), শুরু বা মাঝে ঢোকানো O(n). মাপ জানলে new ArrayList<>(n).
  3. LinkedList প্রায় কখনো নয়: দুই প্রান্তে O(1) হলেও get(i) O(n) আর cache-এ ধীর; queue-র জন্য পর্ব ৩২-এর ArrayDeque.
  4. ঘোরার মধ্যে বাদ: for-each-এ remove নয় — removeIf বা Iterator.remove(). List<Integer>-এ remove(1) মানে index.
  5. কপি বনাম view: List.of, List.copyOf অপরিবর্তনীয় কপি (null নিষেধ); unmodifiableList, subList, Arrays.asList হলো view — মূলটা বদলালে এরাও বদলায়, আর asList-এর মাপ স্থির।

পরের পর্বে: Set আর Map. List-এ contains প্রতিটি জিনিস একে একে দেখে — O(n). দশ লাখ রোল নম্বরের মধ্যে “এটা কি আছে?” প্রশ্নের উত্তর প্রায় এক ধাপে কীভাবে পাওয়া যায়, key দিয়ে মান খোঁজা কীভাবে কাজ করে, আর HashMap বনাম TreeMap — সব পর্ব ৩১-এ।

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

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