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

المُكرِّرات وواجهة Iterable

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

المُكرِّرات وواجهة Iterable

في كل مرة تكتب فيها حلقة for-each على List أو Set، يعمل شيء ما بصمت في الخلفية: بروتوكول Iterator. فهمه يُمكّنك من اجتياز المجموعات بأمان، وحذف العناصر في أثناء الحلقة دون أخطاء، بل وبناء هياكل بيانات قابلة للتكرار بنفسك.

واجهة Iterator

تُعرِّف واجهة java.util.Iterator<E> ثلاث توابع:

  • boolean hasNext() — تُعيد true إذا كانت ثمّة عناصر لم تُزَر بعد.
  • E next() — تُعيد العنصر التالي وتُحرّك المؤشر للأمام.
  • void remove() — تحذف العنصر الذي أعادته آخر استدعاء لـ next() من المجموعة الأصلية (عملية اختيارية).

تحصل على مُكرِّر من أي مجموعة عبر تابعها iterator():

import java.util.ArrayList; import java.util.Iterator; import java.util.List; List<String> cities = new ArrayList<>(List.of("Cairo", "Riyadh", "Dubai", "Amman")); Iterator<String> it = cities.iterator(); while (it.hasNext()) { String city = it.next(); System.out.println(city); }

هذا بالضبط ما تُترجَم إليه حلقة for-each. يُعيد المُصرِّف كتابة for (String c : cities) إلى صيغة while (it.hasNext()) الموضحة أعلاه.

الحذف الآمن للعناصر أثناء التكرار

خطأ شائع للمبتدئين هو استدعاء list.remove() داخل حلقة for-each. يُسبّب هذا رمي استثناء ConcurrentModificationException لأن المجموعة تكتشف أنّها عُدِّلت هيكليًا بينما مُكرِّر نشط.

لا تستدعِ collection.remove() داخل حلقة for-each. تحتفظ حلقة for-each بمُكرِّر ضمني؛ تعديل المجموعة من خارجه يُبطله ويُطلق ConcurrentModificationException.

الأسلوب الصحيح هو استخدام Iterator.remove() بدلًا من ذلك:

List<String> cities = new ArrayList<>(List.of("Cairo", "Riyadh", "Dubai", "Amman")); Iterator<String> it = cities.iterator(); while (it.hasNext()) { String city = it.next(); if (city.startsWith("D")) { it.remove(); // آمن: يحذف "Dubai" دون إبطال المُكرِّر } } System.out.println(cities); // [Cairo, Riyadh, Amman]

القاعدة بسيطة: استدعِ next() دائمًا قبل remove(). استدعاء remove() مرتين متتاليتين دون next() بينهما يُطلق IllegalStateException.

بديل Java 8+: يُعدّ collection.removeIf(predicate) أكثر وضوحًا للحذف الجماعي ويتعامل مع المُكرِّر داخليًا. استخدمه حين لا تحتاج إلى منطق يتجاوز شرطًا بسيطًا: cities.removeIf(c -> c.startsWith("D"));

واجهة Iterable

تمتلك واجهة java.lang.Iterable<T> تابعًا واحدًا مطلوبًا فقط:

public interface Iterable<T> { Iterator<T> iterator(); }

أي صنف يُنفّذ Iterable يمكن استخدامه في حلقة for-each. جميع واجهات المجموعات القياسية (Collection، List، Set، Queue) تمتدّ من Iterable، وهذا هو سبب دعمها كلّها لحلقة for-each.

تنفيذ Iterable على صنف مخصص

افترض أن لديك نوع نطاق بسيط يُمثّل تسلسلًا من الأعداد الصحيحة من start إلى end (غير شاملة). بتنفيذ Iterable<Integer> تحصل على دعم for-each مجانًا:

import java.util.Iterator; import java.util.NoSuchElementException; public class IntRange implements Iterable<Integer> { private final int start; private final int end; public IntRange(int start, int end) { this.start = start; this.end = end; } @Override public Iterator<Integer> iterator() { return new Iterator<>() { private int current = start; @Override public boolean hasNext() { return current < end; } @Override public Integer next() { if (!hasNext()) throw new NoSuchElementException(); return current++; } }; } }

الاستخدام صياغة Java اصطلاحية نظيفة:

for (int n : new IntRange(1, 6)) { System.out.print(n + " "); // 1 2 3 4 5 }
يجب أن يُعيد كل استدعاء لـ iterator() مُكرِّرًا جديدًا مستقلًا. إذا أعدت الكائن ذاته مرتين، تبدأ حلقة for-each الثانية من منتصف التسلسل أو تجده مُستنفَدًا. الصنف الداخلي المجهول أعلاه يلتقط current كمتغيّر محلّي، فكل استدعاء لـ iterator() يُنشئ نسخة جديدة تبدأ من current = start.

ListIterator — الاجتياز ثنائي الاتجاه

تمتدّ java.util.ListIterator<E> من Iterator وتُضيف الاجتياز الخلفي والاستبدال في الموضع ذاته، وهي متاحة على أي List:

List<String> words = new ArrayList<>(List.of("hello", "world", "java")); ListIterator<String> lit = words.listIterator(); while (lit.hasNext()) { String w = lit.next(); lit.set(w.toUpperCase()); // استبدال في الموضع } System.out.println(words); // [HELLO, WORLD, JAVA]

يمكنك أيضًا الاجتياز للخلف عبر hasPrevious() وprevious()، أو إدراج عناصر بـ add().

متى تستخدم Iterator بشكل صريح

في الكود اليومي، فضّل حلقة for-each أو تدفقات Stream. الجأ إلى مُكرِّر صريح فقط حين تحتاج إلى:

  • حذف عناصر أثناء الاجتياز (استخدم Iterator.remove() أو removeIf()).
  • استبدال عناصر في List أثناء الاجتياز (استخدم ListIterator.set()).
  • التناوب بين مُكرِّرَين من المجموعة ذاتها في حلقة واحدة.
  • تنفيذ نوع Iterable مخصص.

الخلاصة

تُوفّر Iterator<E> بروتوكول اجتياز موحّدًا: hasNext()، وnext()، والـ remove() الآمن. أما واجهة Iterable<T> — التي تابعها الوحيد يُعيد Iterator — فهي ما يُفتح حلقة for-each لأي نوع. حين تحتاج إلى حذف عناصر أثناء التكرار، استخدم دائمًا remove() الخاص بالمُكرِّر لا تابع المجموعة. وحين تُصمّم حاوية مخصصة، يُعدّ تنفيذ Iterable الطريقة الأنيقة لمنح العملاء اجتيازًا اصطلاحيًا.