حين يصبح المؤشر مجرد عائق
تمنحك حلقة for ذات العدّاد عدّادًا وشرطًا وخطوة تحديث. لكن في كثير من الأحيان لا يهمّك فعلًا موضع العنصر؛ فكل ما تريده هو أن تفعل شيئًا بكل عنصر، بالترتيب، من البداية إلى النهاية. وإدارة مؤشر لأجل ذلك عمل لا طائل منه، وهو بالضبط المكان الذي تتسلل منه أخطاء الانزياح بمقدار واحد (off-by-one).
تتخلّص حلقة for-each (واسمها في جافا هو for المُحسّنة) من العدّاد تمامًا. تسمّي متغيرًا، وتوجّهه نحو مجموعة، فتسلّمك الحلقة كل عنصر بدوره.
الصياغة الأساسية
شكلها هو for (النوع element : collection). اقرأ النقطتين بمعنى كلمة "في":
لا وجود لـ i، ولا لـ scores.length، ولا لـ scores[i]. في كل دورة، يكون score هو العنصر التالي. تعمل الحلقة مرة واحدة لكل عنصر وتتوقف تلقائيًا حين لا يبقى المزيد؛ فلا يمكنك تجاوز النهاية ولا البدء قبل عنصر واحد مما يجب.
المرور على قائمة
تعمل الحلقة نفسها على أي شيء قابل للتكرار (iterable)، وهذا يشمل List وSet وبقية أنواع المجموعات. يأتي نوع العنصر قبل اسم المتغير:
لاحظ أنك لم تكن بحاجة إلى معرفة أو الاهتمام بما إذا كانت langs مدعومة بمصفوفة أو قائمة مترابطة أو أي شيء آخر؛ فحلقة for-each تعمل بالطريقة نفسها مع جميعها. وهذه قوّتها الحقيقية: صياغة واحدة سهلة القراءة لكل مجموعة.
var يوفّر عليك كتابة اسم النوع
إذا كان نوع العنصر طويلًا أو بديهيًا، فإن var يدع المُصرِّف يستنتجه حتى لا تكرّر نفسك:
من دون var، سيكون متغير الحلقة هو العبارة الثقيلة Map.Entry<String, Integer>. يبقيها var سهلة القراءة، ومع ذلك يُفحَص النوع بالكامل في وقت التصريف؛ فهذا ليس نوعًا مرنًا ديناميكيًا.
مزلق التعديل
ها هي القاعدة التي يقع فيها الجميع: لا يمكنك الإضافة إلى مجموعة أو الحذف منها بينما تمرّ عليها حلقة for-each. فعل ذلك يرمي استثناء ConcurrentModificationException:
List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
if (item.equals("b")) {
items.remove(item); // يرمي ConcurrentModificationException
}
}
تلاحظ الحلقة أن القائمة تغيّرت من تحتها فتتوقف بدلًا من تخطّي العناصر أو تكرارها في صمت. للحذف بأمان، انزل إلى Iterator صريح، الذي يملك remove() تعرفه الحلقة:
ومن الاختصارات الشائعة items.removeIf(item -> item.equals("b"))، الذي يفعل الشيء نفسه في سطر واحد.
للقراءة فقط، لا لإعادة الإسناد
حدّ آخر دقيق: الإسناد إلى متغير الحلقة يغيّر النسخة المحلية فقط، لا المجموعة. وهذا يفاجئ القادمين من لغات يكون فيها متغير الحلقة مرجعًا حيًّا:
إذا احتجت إلى الكتابة مجددًا في المصفوفة، فأنت بحاجة إلى المؤشر، وهذا يعني حلقة for التقليدية ذات العدّاد: for (int i = 0; i < nums.length; i++) nums[i] = nums[i] * 10;. وبالنسبة لعناصر الكائنات، يمكنك تعديل الكائن الذي يشير إليه المتغير (باستدعاء دالة ضبط setter مثلًا)، لكن لا يمكنك استبداله داخل المجموعة.
break و continue ما زالا يعملان
حلقة for-each حلقة حقيقية، لذا يتصرّف break وcontinue تمامًا كما في أي مكان آخر؛ فـ break يغادر الحلقة، وcontinue يقفز إلى العنصر التالي:
يطبع هذا keep ثم keep؛ إذ يتخطّى "skip" ويتوقف عند "stop" قبل أن يبلغ "never". فأنت إذن لست مُلزَمًا بزيارة كل عنصر؛ إنما تتخلّى عن المؤشر مقابل شيفرة أنظف.
التالي: المصفوفات
لقد مررت الآن على المصفوفات عدة مرات دون التوقف عند ماهيتها الحقيقية؛ فهي حاويات ذات حجم ثابت ومفهرسة تحمل ذلك الحقل .length. تعود الصفحة التالية إلى البداية وتتناول المصفوفات كما ينبغي: كيفية التصريح بها، والقيم الافتراضية التي تبدأ بها، وكيف يختلف حجمها الثابت عن ArrayList القابلة للنمو.
الأسئلة الشائعة
ما هي حلقة for-each في جافا؟
حلقة for-each (وتُسمى أيضًا for المُحسّنة) تمرّ على كل عنصر في مصفوفة أو مجموعة دون عدّاد: for (النوع item : collection) { ... }. اقرأها على أنها "لكل عنصر في المجموعة". وهي أنظف من حلقة for ذات العدّاد عندما تحتاج إلى كل عنصر فقط ولا تحتاج إلى المؤشر مطلقًا.
ما الفرق بين حلقة for وحلقة for-each في جافا؟
حلقة for التقليدية تستخدم عدّادًا صريحًا (for (int i = 0; i < arr.length; i++))، فتتحكم أنت في المؤشر والاتجاه. أما حلقة for-each، for (النوع x : arr)، فلا مؤشر لها؛ إذ تزور كل عنصر بالترتيب. استخدم for-each للمرور للقراءة فقط من البداية إلى النهاية؛ واستخدم الحلقة ذات العدّاد عندما تحتاج إلى المؤشر، أو تريد تخطّي عناصر، أو تعديل بنية المجموعة.
لماذا ترمي حلقة for-each الخاصة بي استثناء ConcurrentModificationException؟
لأنك استدعيت add() أو remove() على المجموعة أثناء تكرارها بحلقة for-each. تكتشف الحلقة التغيير البنيوي وترمي الاستثناء لحمايتك من سلوك غير محدد. لحذف العناصر بأمان، استخدم Iterator صريحًا وطريقته remove()، أو اجمع العناصر المراد حذفها ثم احذفها بعد انتهاء الحلقة.