«أريد إزالة عنصر من القائمة في حلقة – وأحصل على ConcurrentModificationException». هل هذا مألوف؟
\nإذا كنت قد كتبت بلغة جافا مرة واحدة على الأقل وحاولت إزالة عنصر من ArrayList أو HashMap أثناء التكرار (على سبيل المثال، في حلقة for-each)، فمن المؤكد أنك رأيت هذا الاستثناء الأحمر المخيف. يظهر في أكثر الأوقات غير المناسبة، وكأن جافا تقول لك: «مهلاً، ماذا تفعل؟ لا تجرؤ على لمس المجموعة بينما أنا أمرّ عبرها!».
على Stack Overflow، جمع هذا السؤال آلاف المشاهدات. وهذا ليس من قبيل الصدفة: المشكلة تبدو بسيطة، ولكن تحت الغطاء يكمن طبقة كاملة من التحسينات والفروق الدقيقة في تعدد الخيوط. في هذه المقالة، سنشرح كيفية إزالة الكائنات من المجموعة في حلقة جافا بشكل صحيح، دون التسبب في ConcurrentModificationException، ودون فقدان الأداء.
هيا بنا!
\n\nلماذا يحدث ConcurrentModificationException؟
\nالإجابة المختصرة: المكررات سريعة الفشل (fail-fast iterators). معظم المجموعات من إطار عمل مجموعات جافا (ArrayList, HashSet, HashMap وغيرها) تستخدم عداد تغييرات داخلي (modCount). عندما تقوم بإنشاء مكرر، فإنه يحفظ القيمة الحالية لـ modCount. في كل مرة تستدعي next()، يتحقق المكرر مما إذا كانت المجموعة قد تغيرت من الخارج (عبر طرق add() أو remove() الخاصة بالمجموعة). إذا اختلف العداد، يظهر الاستثناء.
مثال على كود سيفشل بالتأكيد:
\nList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));\nfor (String item : list) {\n if ("B".equals(item)) {\n list.remove(item); // ConcurrentModificationException!\n }\n}\n\nلماذا؟ لأن for-each يستخدم ضمنياً مكرراً، و list.remove() يغير modCount مباشرة، متجاوزاً المكرر. المكرر لا يغفر هذا.
الطريقة رقم 1: استخدام Iterator.remove() – الطريقة الكلاسيكية
\nأبسط وأصح طريقة هي أخذ المكرر بين يديك بشكل صريح واستدعاء طريقته remove(). هذه الطريقة تقوم بمزامنة العداد الداخلي للمكرر مع المجموعة.
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));\nIterator<String> iterator = list.iterator();\nwhile (iterator.hasNext()) {\n String item = iterator.next();\n if ("B".equals(item)) {\n iterator.remove(); // آمن!\n }\n}\nSystem.out.println(list); // [A, C, D]\n\nالإيجابيات: آمن، يعمل مع جميع المجموعات التي تدعم المكررات. السلبيات: مطول قليلاً. للتصفيات البسيطة، يمكن استخدام طرق أكثر حداثة.
\n\nالطريقة رقم 2: Collection.removeIf() – حديثة وأنيقة
\nبدءاً من جافا 8، ظهرت في معظم المجموعات طريقة removeIf(Predicate filter). هذه هي الطريقة الأكثر إيجازاً لإزالة العناصر وفق شرط.
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));\nlist.removeIf(item -> "B".equals(item));\nSystem.out.println(list); // [A, C, D]\n\nداخلياً، تستخدم removeIf مكرراً (أو تمريراً مباشراً مع حفظ الفهارس)، لذا لن يحدث أي ConcurrentModificationException. هذه هي الطريقة الموصى بها للحالات البسيطة.
الأداء: بالنسبة لـ ArrayList، تعمل طريقة removeIf بزمن O(n)، ولكنها تفعل ذلك بكفاءة – فهي تنقل العناصر في تمريرة واحدة، بدلاً من إزالتها واحدة تلو الأخرى (والتي كانت ستكون O(n²)).
الطريقة رقم 3: المرور العكسي بالفهرس (لـ List)
\nإذا كنت لسبب ما لا تستطيع استخدام المكرر (على سبيل المثال، تحتاج إلى تغيير عدة عناصر في وقت واحد أو تعمل مع كود قديم)، يمكنك المرور عبر القائمة بترتيب عكسي باستخدام حلقة for عادية مع فهرس.
List<String> list = new A