Menu
flag Ar iconالعربيةdown icon

المرور على المجموعات في Java: for-each وIterator وforEach

طرق المرور على مجموعات Java - حلقة for-each، وIterator، وحلقات الفهرسة، ودالة forEach - وكيفية حذف العناصر بأمان أثناء المرور.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

مهمة واحدة وعدة أدوات

لقد جمعت بعض البيانات - في ArrayList أو HashSet أو HashMap - والآن تريد زيارة كل عنصر. تمنحك Java عدة طرق للقيام بذلك، والطريقة التي تختارها تعتمد على ما إذا كنت تحتاج إلى الفهرس، وما إذا كنت تحتاج إلى حذف عناصر في منتصف الحلقة، وما إذا كنت تفضّل صياغة على هيئة دالة أم على هيئة حلقة.

والخبر الجيد: كل Collection (قائمة أو مجموعة أو طابور) تدعم نفس حلقة for المحسّنة، فبمجرد أن تتعلمها يمكنك المرور عليها جميعاً بالطريقة نفسها.

اقرأ النقطتين بمعنى «في»: «لكل lang في langs». أنت لا تتعامل مع فهرس إطلاقاً، فلا يوجد ما يمكن أن تخطئ فيه.

حلقة for-each

حلقة for المحسّنة هي الخيار الافتراضي للحالة البسيطة «افعل شيئاً بكل عنصر». تُقرأ بوضوح وتعمل بالطريقة نفسها عبر أنواع المجموعات - وهنا HashSet لا يملك فهارس على الإطلاق:

أمر يجدر تذكّره بخصوص HashSet: ليس له ترتيب محدّد، لذا قد تُطبع العناصر بأي تسلسل. ومع ذلك تزور حلقة for-each كل عنصر مرة واحدة بالضبط.

حين تحتاج إلى الفهرس

تمنحك حلقة for-each العنصر لكن ليس موضعه. إذا كنت تحتاج إلى الفهرس فعلاً - لترقيم الأسطر أو للنظر إلى العناصر المجاورة - فاستخدم حلقة معدودة مع size() وget(i). هذا يعمل على List لأنها قائمة على المواضع؛ أما المجموعات والخرائط فلا فهرس لها، لذا لا ينطبق هذا الأسلوب عليها.

لا تلجأ إلى هذا بدافع العادة فحسب. إذا لم تكن تستخدم i في شيء سوى get(i)، فإن نسخة for-each أقصر وأصعب على الخطأ.

المرور على Map

إن Map ليست Collection، لذا لا يمكنك تطبيق for-each عليها مباشرة. بدلاً من ذلك تمرّ على إحدى عروضها الثلاثة. أكثرها شيوعاً هو entrySet()، الذي يسلّمك كل زوج مفتاح-قيمة معاً:

إذا كنت تحتاج إلى المفاتيح فقط، فمرّ على ages.keySet()؛ وإذا كنت تحتاج إلى القيم فقط، فمرّ على ages.values(). فضّل entrySet() حين تحتاج إلى كليهما - فالمرور على المفاتيح ثم استدعاء ages.get(key) في الداخل يُجري بحثاً ثانياً في كل تكرار دون أي داعٍ.

الـ Iterator

حلقة for-each هي في الحقيقة سكّر نحوي فوق Iterator - وهو كائن يسير عبر المجموعة عنصراً تلو الآخر عبر hasNext() وnext(). نادراً ما تكتب هذه الحلقة يدوياً، مع استثناء واحد مهم: إنها الطريقة الآمنة لحذف العناصر أثناء المرور.

تحذف it.remove() العنصر الذي أعادته next() آخر مرة، ويبقى المكرِّر صالحاً. هذه هي الطريقة المُجازة الوحيدة لتعديل مجموعة أثناء حلقة مكتوبة يدوياً.

الفخّ: ConcurrentModificationException

إذا استدعيت add أو remove على المجموعة نفسها داخل حلقة for-each، فستحصل على ConcurrentModificationException - يلاحظ المكرِّر أن المجموعة تغيّرت من تحته فيرفض المتابعة. وهذا من أكثر الأخطاء شيوعاً لدى المبتدئين.

List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
for (int n : nums) {
    if (n % 2 == 0) {
        nums.remove(Integer.valueOf(n));   // throws ConcurrentModificationException
    }
}

الحل في الغالب هو removeIf، الذي يعبّر عن القصد في سطر واحد ويتولّى المرور نيابةً عنك:

تعمل removeIf على أي Collection، لذا فإن الاستدعاء نفسه ينظّف HashSet أيضاً.

دالة forEach

تملك كل مجموعة أيضاً دالة forEach تأخذ تعبير lambda وتشغّله على كل عنصر. وهي بديل أكثر اتّباعاً للنمط الوظيفي وعلى هيئة تعبير عن الحلقة - مفيدة للأسطر القصيرة المفردة:

لاحظ أن Map.forEach تأخذ تعبير lambda بمعاملين (key, value) مباشرة - دون الحاجة إلى entrySet(). استخدم forEach للآثار الجانبية السريعة؛ وارجع إلى حلقة for العادية حين يكبر جسم الحلقة أو حين تحتاج إلى break للخروج مبكراً، وهو ما لا يستطيع تعبير lambda فعله.

التالي: الدوال (Methods)

لقد عبّأت الآن البيانات في مجموعات ومررت عليها بكل طريقة تقدّمها Java. الخطوة التالية هي تعبئة السلوك: كتابة دوالك الخاصة لتتمكّن من تسمية كتلة من المنطق وتمرير المدخلات إليها وإعادة استخدامها - وهذا ما تتناوله الصفحة التالية.

الأسئلة الشائعة

كيف تمرّ على قائمة في Java؟

أنظف طريقة هي حلقة for المحسّنة (for-each): for (String s : list) { ... }. وهي تعمل على أي Collection - ArrayList وHashSet وغيرها. استخدم حلقة بالفهرس مع get(i) فقط حين تحتاج الموضع فعلاً، واستخدم Iterator حين تحتاج إلى حذف عناصر أثناء الحلقة.

كيف تمرّ على HashMap في Java؟

إن Map ليست Collection، لذا تمرّ على إحدى عروضها (views). الخيار المعتاد هو for (Map.Entry<K, V> e : map.entrySet())، الذي يعطيك المفتاح (e.getKey()) والقيمة (e.getValue()) معاً في مرور واحد. يمكنك أيضاً المرور على map.keySet() للمفاتيح أو map.values() للقيم.

لماذا أحصل على ConcurrentModificationException أثناء الحلقة؟

لقد استدعيت add أو remove على المجموعة بينما كانت حلقة for-each تمرّ عليها. تستخدم حلقة for-each كائن Iterator خلف الكواليس، وهو يكتشف أن المجموعة تغيّرت بنيوياً. أصلح ذلك بالحذف عبر دالة remove() الخاصة بـIterator نفسه، أو باستدعاء removeIf(...) بدلاً من الحلقة.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن