إطار المجموعات

ArrayList بالتفصيل

15 دقيقة الدرس 2 من 14

ArrayList بالتفصيل

ArrayList هي المجموعة الأكثر استخدامًا في Java. تُقدّم مصفوفة قابلة للتمدد مبنيةً فوق مصفوفة Object[] عادية في الخفاء، مما يمنحك وصولًا سريعًا بالفهرس مع تحريرك من إدارة نموّ المصفوفة يدويًا. فهم آلية عملها — لا مجرّد حفظ الدوال — يجعلك مهندسًا أفضل بكثير.

إنشاء ArrayList

تقريبًا في كل الحالات تُبرمج بالتعامل مع واجهة List وتُعيّن كائن ArrayList. هذا يُبقي كودك مرنًا: إن احتجت لاحقًا إلى LinkedList فلا يتغيّر سوى المُنشئ.

import java.util.ArrayList; import java.util.List; // الأفضل: تُعلن بنوع List وتُنشئ كـ ArrayList List<String> languages = new ArrayList<>(); // إن كنت تعرف الحجم التقريبي مسبقًا، مرّر سعة ابتدائية List<Integer> scores = new ArrayList<>(50);
استخدم نوع الواجهة على اليسار. كتابة List<String> list = new ArrayList<>() بدلًا من ArrayList<String> list = new ArrayList<>() يتيح لك استبدال التنفيذ لاحقًا دون تعديل نقاط الاستدعاء.

الدوال الأربع الأساسية للتعديل

add وset وremove وget هي أدوات العمل اليومية.

List<String> fruits = new ArrayList<>(); // add(E e) — يُضيف إلى النهاية، O(1) مُهلَّل fruits.add("apple"); fruits.add("banana"); fruits.add("cherry"); // ["apple", "banana", "cherry"] // add(int index, E e) — يُدرج في موضع معيّن، O(n) لأن العناصر تتزحزح يمينًا fruits.add(1, "blueberry"); // ["apple", "blueberry", "banana", "cherry"] // get(int index) — استرجاع بالفهرس، O(1) String first = fruits.get(0); // "apple" // set(int index, E e) — يستبدل في الفهرس، O(1)، يُعيد القيمة القديمة String old = fruits.set(2, "mango"); // old = "banana" // ["apple", "blueberry", "mango", "cherry"] // remove(int index) — يحذف بالموضع، O(n) بسبب الإزاحة fruits.remove(0); // ["blueberry", "mango", "cherry"] // remove(Object o) — يحذف أوّل ظهور بالقيمة، O(n) fruits.remove("mango"); // ["blueberry", "cherry"]
شكلان لـ remove مع الأعداد الصحيحة. مع List<Integer>، يحذف list.remove(2) العنصر في الفهرس 2 لا القيمة 2. لحذف القيمة، غلّفها: list.remove(Integer.valueOf(2)).

كيف يعمل التمدد الديناميكي

تبدأ ArrayList بمصفوفة داخلية بسعة 10 (الافتراضية). حين تُضيف عنصرًا والمصفوفة ممتلئة، تُخصّص Java مصفوفة جديدة أكبر بنحو 1.5×، تنسخ فيها كل العناصر، وتتخلّص من القديمة. هذا النسخ O(n) لكنه يحدث نادرًا، فتكلفة add المُهلَّلة تبقى O(1).

// إجبار القائمة على النمو عدة مرات List<Integer> numbers = new ArrayList<>(); // السعة الداخلية = 10 for (int i = 0; i < 100; i++) { numbers.add(i); // يُثير تمددات عند ~10، ~15، ~22، ~33 ... حتى السعة > 100 } System.out.println(numbers.size()); // 100 // إن كنت تعرف الحجم النهائي، حدّد الحجم مسبقًا لتجنّب التمددات List<Integer> preSized = new ArrayList<>(100);

دوال الأدوات المفيدة

List<String> items = new ArrayList<>(List.of("x", "y", "z", "y")); int sz = items.size(); // 4 boolean empty = items.isEmpty(); // false boolean has = items.contains("y"); // true int idx = items.indexOf("y"); // 1 (أوّل ظهور) int last = items.lastIndexOf("y"); // 3 (آخر ظهور) items.clear(); // يحذف جميع العناصر System.out.println(items.isEmpty()); // true

التكرار على ArrayList

توجد ثلاث طرق اصطلاحية للتكرار. حلقة for-each المُعزَّزة هي الأنظف للمرورات القرائية فقط؛ حلقة الفهرس ضرورية حين تحتاج الموضع؛ وforEach مع تعبير lambda معبّرة للعمليات البسيطة.

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Carol")); // 1. حلقة for-each المُعزَّزة — نظيفة، للقراءة فقط for (String name : names) { System.out.println(name); } // 2. حلقة الفهرس — حين تحتاج الموضع for (int i = 0; i < names.size(); i++) { System.out.println(i + ": " + names.get(i)); } // 3. forEach مع lambda — موجزة للعمليات البسيطة names.forEach(name -> System.out.println(name.toUpperCase()));
لا تحذف عناصر أثناء التكرار بحلقة for-each. سيُثير ذلك ConcurrentModificationException. استخدم Iterator صريحًا واستدعِ iterator.remove()، أو اجمع ما تريد حذفه أولًا ثم استدعِ list.removeAll(toRemove).

القائمة الفرعية والعمليات الجماعية

List<Integer> nums = new ArrayList<>(List.of(10, 20, 30, 40, 50)); // subList تُعيد عرضًا VIEW — التغييرات تؤثر على الأصل List<Integer> middle = nums.subList(1, 4); // [20, 30, 40] middle.clear(); // nums أصبحت [10, 50] // addAll — يُضيف مجموعة أخرى بالكامل List<String> a = new ArrayList<>(List.of("one", "two")); List<String> b = List.of("three", "four"); a.addAll(b); // ["one", "two", "three", "four"] // removeIf — يحذف كل العناصر المطابقة لشرط (Java 8+) a.removeIf(s -> s.startsWith("t")); // ["one", "four"]

متى تستخدم ArrayList

اختر ArrayList حين:

  • تحتاج وصولًا سريعًا بالفهرسget(i) هو O(1).
  • تُضيف غالبًا إلى النهايةadd(e) هو O(1) مُهلَّل.
  • القراءات تفوق بكثير الكتابات، أو الوصول العشوائي متكرّر.

فكّر في البدائل حين:

  • تُدرج أو تحذف كثيرًا في الوسط — الإزاحة O(n)؛ قد تكون LinkedList أو Deque أسرع حسب أنماط الوصول.
  • تحتاج سلامة الخيوط — استخدم Collections.synchronizedList أو CopyOnWriteArrayList.
  • تحتاج الفردانية — استخدم Set بدلًا من ذلك.

الخلاصة

تجمع ArrayList بين الوصول العشوائي O(1) كالمصفوفة والنموّ التلقائي. اعرف تعقيداتك: get وadd في النهاية هما O(1)؛ الإدراج والحذف في الوسط هما O(n). أعلن المتغيّر كـ List، وحدّد الحجم مسبقًا حين تعرفه، وتجنّب تعديل القائمة داخل حلقة for-each.