مقدمة: لماذا أصبحت حلقة for-each "ملك" الحلقات في Java؟
\n\nإذا كنت قد كتبت كودًا بلغة Java لمدة أسبوع على الأقل، فبالتأكيد صادفت الصياغة for (Type var : collection). هذا التركيب المختصر يُسمى حلقة for-each (أو الحلقة المحسّنة enhanced for loop). ظهرت لأول مرة في Java 5 (عام 2004) وسرعان ما أصبحت المفضلة لدى المطورين.
ولكن هل تعرف ما يحدث تحت الغطاء عندما تواجه JVM هذه الصياغة؟ لماذا قد تكون حلقة for-each أبطأ من الحلقة العادية for مع المصفوفات؟ ولماذا ترمي أحيانًا استثناء ConcurrentModificationException حتى لو لم تقم بحذف أي شيء؟
في هذا المقال، سنشرح كيف تعمل حلقة for-each في Java على مستوى البايت كود، ونقارنها بالحلقات التقليدية، ونتعلم كيفية تجنب الأخطاء الشائعة. هيا بنا!
\n\nالصياغة والتعريف الأولي
\n\nالصياغة الأساسية هي:
\n\nfor (نوع_العنصر متغير : مجموعة_أو_مصفوفة) {\n // جسم الحلقة\n}\n\nمثال مع ArrayList:
List<String> names = Arrays.asList("أحمد", "سارة", "خالد");\n\nfor (String name : names) {\n System.out.println("مرحبًا، " + name + "!");\n}\n\nالنتيجة:
\n\nمرحبًا، أحمد!\nمرحبًا، سارة!\nمرحبًا، خالد!\n\nالأمر بسيط، أليس كذلك؟ لكن خلف هذه البساطة يختبئ آليتان مختلفتان تمامًا — واحدة للمصفوفات وأخرى للمجموعات.
\n\nكيف تعمل حلقة for-each مع المصفوفات؟
\n\nعند كتابة حلقة for-each لمصفوفة، يقوم مترجم Java بتحويلها إلى حلقة for عادية بمؤشر (index). لا يوجد أي سحر.
الكود الخاص بك:
\n\nint[] numbers = {10, 20, 30};\n\nfor (int num : numbers) {\n System.out.println(num);\n}\n\nبعد الترجمة (قم بفك ترجمة البايت كود — لتتأكد):
\n\nint[] numbers = new int[]{10, 20, 30};\nfor (int i = 0; i < numbers.length; i++) {\n int num = numbers[i];\n System.out.println(num);\n}\n\nاستنتاج مهم: بالنسبة للمصفوفات، حلقة for-each والحلقة العادية for متطابقتان في الأداء. لا توجد تكاليف إضافية للمحولات (iterators).
\n\nكيف تعمل حلقة for-each مع المجموعات (List, Set)؟
\n\nهنا يبدأ الجزء المثير للاهتمام. بالنسبة للمجموعات، تستخدم حلقة for-each المحول (iterator).
\n\nالكود الخاص بك:
\n\nList<String> list = new ArrayList<>();\nlist.add("Java");\nlist.add("Python");\nlist.add("C++");\n\nfor (String lang : list) {\n System.out.println(lang);\n}\n\nيقوم المترجم بتحويل هذا إلى:
\n\nList<String> list = new ArrayList<>();\nlist.add("Java");\nlist.add("Python");\nlist.add("C++");\n\nfor (Iterator<String> it = list.iterator(); it.hasNext(); ) {\n String lang = it.next();\n System.out.println(lang);\n}\n\nأي:
\n- \n
- يتم استدعاء
list.iterator()— نحصل على المحول. \n - في الشرط —
it.hasNext(). \n - في الجسم —
it.next(). \n - يتم تهيئة المتغير
langبنتيجةnext(). \n
ماذا يعني هذا عمليًا؟
\n- \n
- لا يمكن حذف العناصر داخل حلقة for-each باستخدام
list.remove()— ستحصل علىConcurrentModificationException. استخدمit.remove()أو الحلقة العادية for. \n - لا يمكن تعديل المجموعة (إضافة/حذف) أثناء التكرار — نفس الخطأ. \n
- الأداء: بالنسبة لـ
ArrayList، الفرق مع الحلقة العادية for ضئيل (إنشاء كائن المحول). بالنسبة لـLinkedList— حلقة for-each أسرع بكثير منfor (int i = 0; i < list.size(); i++)، لأنget(i)في LinkedList هو O(n) في كل مرة! \n
ماذا عن Map؟ التكرار عبر أزواج المفتاح-القيمة
\n\nلا تطبق Map واجهة Iterable بشكل مباشر، لذا فإن حلقة for-each