لماذا قد يتباطأ كودك فجأة؟
\nتخيل: لقد كتبت خوارزمية مثالية، واجتزت جميع اختبارات الوحدة، ولكن في بيئة الإنتاج يبدأ التطبيق فجأة في "التباطؤ". في أغلب الأحيان، لا يكون السبب هو الخوارزمية نفسها، بل الطريقة التي تجتاز بها المجموعات. وهذا ينطبق بشكل خاص على Java Map - بنية البيانات التي تظهر في 9 من أصل 10 مشاريع مؤسسية.
\nسؤال "How do I efficiently iterate over each entry in a Java Map?" على Stack Overflow حصل على عشرات الآلاف من المشاهدات ليس من قبيل الصدفة. العديد من المطورين، حتى مع خبرة 2-3 سنوات، يستخدمون طرقًا غير مثلى. دعنا نفهم كيفية اجتياز Map بشكل صحيح، ولماذا يمكن أن تكون إحدى الطرق أسرع بعشر مرات من الأخرى.
\n\nتشريح Java Map: ما بداخلها؟
\nقبل الحديث عن التكرار، دعنا نتذكر البنية. HashMap هي جدول تجزئة مع دلاء. TreeMap هي شجرة حمراء-سوداء. LinkedHashMap هي هجين مع قائمة مزدوجة الارتباط. كل من هذه الهياكل تخزن أزواج key-value، لكن طرق اجتيازها تختلف في السرعة واستهلاك الذاكرة.
المجموعات العرضية الرئيسية (view collections) التي يوفرها Map:
\n- \n
keySet()- مجموعة المفاتيح \n values()- مجموعة القيم \n entrySet()- مجموعة الأزواج (مفتاح-قيمة) \n
من خلال هذه "النوافذ" الثلاث سننظر إلى البيانات.
\n\nالطريقة رقم 1: entrySet الكلاسيكي عبر for-each (الأكثر شيوعًا)
\nهذا نمط أساسي ولكنه فعال جدًا. تحصل على الوصول إلى كل من المفتاح والقيمة في تكرار واحد.
\nMap<String, Integer> map = new HashMap<>();\n// ... ملء الخريطة\nfor (Map.Entry<String, Integer> entry : map.entrySet()) {\n String key = entry.getKey();\n Integer value = entry.getValue();\n // منطقك\n}\nلماذا هذا فعال؟ لأن entrySet() يعيد view (عرضًا) وليس نسخة من البيانات. يتنقل المكرر عبر العقد الداخلية (Node) لـ HashMap، وكل استدعاء لـ next() يعطي فورًا كلاً من المفتاح والقيمة. لا يوجد بحث إضافي عن التجزئة.
متى تستخدم: دائمًا عندما تحتاج إلى كلا مكوني الزوج. هذا هو المعيار الفعلي في مجتمع Java.
\n\nالطريقة رقم 2: keySet + get (نمط مضاد يجب تجنبه)
\nيكتب العديد من المبتدئين بهذه الطريقة:
\nfor (String key : map.keySet()) {\n Integer value = map.get(key);\n // منطق\n}\nللوهلة الأولى، الكود قابل للقراءة. لكن داخليًا، HashMap.get() يعيد حساب تجزئة المفتاح في كل مرة ويمر عبر سلسلة التصادمات. بالنسبة لـ 100 عنصر، هذا غير ملحوظ، ولكن بالنسبة لـ 100,000 - يحدث تدهور خطير. في أسوأ الحالات (الكثير من التصادمات)، يتحول التعقيد من O(n) إلى O(n²).
\nالخلاصة: إذا كنت بحاجة إلى كل من المفتاح والقيمة - استخدم entrySet(). إذا كنت بحاجة إلى المفاتيح فقط - فإن keySet() مثالي.
الطريقة رقم 3: المكرر مع الحذف الصريح (للتحورات أثناء الاجتياز)
\nأحيانًا تحتاج إلى حذف عناصر من Map أثناء التكرار. حلقة for-each ستلقي ConcurrentModificationException. هنا يأتي المكرر للإنقاذ:
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();\nwhile (iterator.hasNext()) {\n Map.Entry<String, Integer> entry = iterator.next();\n if (entry.getValue() < 0) {\n iterator.remove(); // حذف آمن\n }\n}\nهذه الطريقة مفيدة بشكل خاص عندما تقوم بتصفية البيانات "أثناء التنقل". يعمل المكرر بتعقيد O(1) لكل عملية حذف في HashMap، لأنه يعرف العقدة الحالية.
\n\nالطريقة رقم 4: forEach مع لامبدا (Java 8+)
\nابتداءً من Java 8، أصبح لدى Map طريقة خاصة بها forEach:
map.forEach((key, value) -> {\n System.out.println(key + " -> " + value);\n});\nتحت الغطاء