اجتياز Map في Java: كيفية التكرار بدون ألم وفقدان السرعة

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

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

ابدأ الدورة

لماذا قد يتباطأ كودك فجأة؟

\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، لكن طرق اجتيازها تختلف في السرعة واستهلاك الذاكرة.

\n

المجموعات العرضية الرئيسية (view collections) التي يوفرها Map:

\n
    \n
  • keySet() - مجموعة المفاتيح
  • \n
  • values() - مجموعة القيم
  • \n
  • entrySet() - مجموعة الأزواج (مفتاح-قيمة)
  • \n
\n

من خلال هذه "النوافذ" الثلاث سننظر إلى البيانات.

\n\n

الطريقة رقم 1: entrySet الكلاسيكي عبر for-each (الأكثر شيوعًا)

\n

هذا نمط أساسي ولكنه فعال جدًا. تحصل على الوصول إلى كل من المفتاح والقيمة في تكرار واحد.

\n
Map<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() يعطي فورًا كلاً من المفتاح والقيمة. لا يوجد بحث إضافي عن التجزئة.

\n

متى تستخدم: دائمًا عندما تحتاج إلى كلا مكوني الزوج. هذا هو المعيار الفعلي في مجتمع Java.

\n\n

الطريقة رقم 2: keySet + get (نمط مضاد يجب تجنبه)

\n

يكتب العديد من المبتدئين بهذه الطريقة:

\n
for (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() مثالي.

\n\n

الطريقة رقم 3: المكرر مع الحذف الصريح (للتحورات أثناء الاجتياز)

\n

أحيانًا تحتاج إلى حذف عناصر من Map أثناء التكرار. حلقة for-each ستلقي ConcurrentModificationException. هنا يأتي المكرر للإنقاذ:

\n
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:

\n
map.forEach((key, value) -> {\n    System.out.println(key + " -> " + value);\n});
\n

تحت الغطاء

المدونات

توصيات الكتب