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.