إزالة العناصر من المجموعة في جافا: كيفية تجنب ConcurrentModificationException

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

«أريد إزالة عنصر من القائمة في حلقة – وأحصل على ConcurrentModificationException». هل هذا مألوف؟

\n

إذا كنت قد كتبت بلغة جافا مرة واحدة على الأقل وحاولت إزالة عنصر من ArrayList أو HashMap أثناء التكرار (على سبيل المثال، في حلقة for-each)، فمن المؤكد أنك رأيت هذا الاستثناء الأحمر المخيف. يظهر في أكثر الأوقات غير المناسبة، وكأن جافا تقول لك: «مهلاً، ماذا تفعل؟ لا تجرؤ على لمس المجموعة بينما أنا أمرّ عبرها!».

\n\n

على Stack Overflow، جمع هذا السؤال آلاف المشاهدات. وهذا ليس من قبيل الصدفة: المشكلة تبدو بسيطة، ولكن تحت الغطاء يكمن طبقة كاملة من التحسينات والفروق الدقيقة في تعدد الخيوط. في هذه المقالة، سنشرح كيفية إزالة الكائنات من المجموعة في حلقة جافا بشكل صحيح، دون التسبب في ConcurrentModificationException، ودون فقدان الأداء.

\n\n

هيا بنا!

\n\n

لماذا يحدث ConcurrentModificationException؟

\n

الإجابة المختصرة: المكررات سريعة الفشل (fail-fast iterators). معظم المجموعات من إطار عمل مجموعات جافا (ArrayList, HashSet, HashMap وغيرها) تستخدم عداد تغييرات داخلي (modCount). عندما تقوم بإنشاء مكرر، فإنه يحفظ القيمة الحالية لـ modCount. في كل مرة تستدعي next()، يتحقق المكرر مما إذا كانت المجموعة قد تغيرت من الخارج (عبر طرق add() أو remove() الخاصة بالمجموعة). إذا اختلف العداد، يظهر الاستثناء.

\n\n

مثال على كود سيفشل بالتأكيد:

\n
List<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 مباشرة، متجاوزاً المكرر. المكرر لا يغفر هذا.

\n\n

الطريقة رقم 1: استخدام Iterator.remove() – الطريقة الكلاسيكية

\n

أبسط وأصح طريقة هي أخذ المكرر بين يديك بشكل صريح واستدعاء طريقته remove(). هذه الطريقة تقوم بمزامنة العداد الداخلي للمكرر مع المجموعة.

\n
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). هذه هي الطريقة الأكثر إيجازاً لإزالة العناصر وفق شرط.

\n
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. هذه هي الطريقة الموصى بها للحالات البسيطة.

\n

الأداء: بالنسبة لـ ArrayList، تعمل طريقة removeIf بزمن O(n)، ولكنها تفعل ذلك بكفاءة – فهي تنقل العناصر في تمريرة واحدة، بدلاً من إزالتها واحدة تلو الأخرى (والتي كانت ستكون O(n²)).

\n\n

الطريقة رقم 3: المرور العكسي بالفهرس (لـ List)

\n

إذا كنت لسبب ما لا تستطيع استخدام المكرر (على سبيل المثال، تحتاج إلى تغيير عدة عناصر في وقت واحد أو تعمل مع كود قديم)، يمكنك المرور عبر القائمة بترتيب عكسي باستخدام حلقة for عادية مع فهرس.

\n
List<String> list = new A                

المدونات

توصيات الكتب