إطار المجموعات

HashMap بعمق

15 دقيقة الدرس 6 من 14

HashMap بعمق

HashMap هي التطبيق الأكثر استخدامًا لواجهة Map في Java. تخزّن البيانات كـأزواج مفتاح-قيمة، وتوفّر بحثًا بمعدل O(1) في المتوسط بالمفتاح، وهي الأداة المناسبة كلّما احتجت إلى ربط شيء بشيء آخر — اسم مستخدم بملف شخصي، أو كلمة بتكرارها، أو معرّف منتج بسعره.

واجهة Map

قبل التعامل مع HashMap مباشرةً، افهم ما تعِده واجهة Map. على خلاف List أو Set، فإنّ Map ليست Collection — لها تسلسل هرمي مستقل. يقول العقد:

  • كل مفتاح يرتبط بقيمة واحدة فقط.
  • المفاتيح فريدة؛ وضع المفتاح نفسه مرّتين يستبدل القيمة القديمة.
  • القيم لا يشترط أن تكون فريدة — يمكن لمفاتيح متعددة أن تشير إلى القيمة ذاتها.
  • يمكن أن يرتبط مفتاح بـnull، كما أن null نفسه مفتاح صالح في HashMap.
Map ليست Collection. لا تمتد Map<K,V> من Collection<E>. لا يمكنك تمرير Map حيث تُتوقّع Collection. غير أنه يمكنك استدعاء map.values() أو map.keySet() أو map.entrySet() للحصول على طرق عرض كمجموعات.

إنشاء HashMap

أعلِن دائمًا بنوع الواجهة على اليسار حتى تتمكّن من تبديل التطبيقات لاحقًا:

import java.util.HashMap; import java.util.Map; Map<String, Integer> wordCount = new HashMap<>();

يتيح عامل الماس <> للمُترجم استنتاج الوسيطات. يمكنك أيضًا ملء الخريطة عند الإنشاء في Java 9+:

// مصنع ثابت — مفيد للخرائط الصغيرة الثابتة Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 82, "Carol", 78);

العمليات الأساسية: put و get و remove

put(key, value) تضيف إدخالًا أو تستبدله وتُعيد القيمة السابقة (أو null إن كان المفتاح غائبًا). get(key) تُعيد القيمة أو null. remove(key) تحذف الإدخال وتُعيد القيمة المحذوفة.

Map<String, Integer> stock = new HashMap<>(); stock.put("apples", 50); stock.put("bananas", 30); stock.put("apples", 60); // يستبدل 50 — المفتاح "apples" يصبح 60 Integer prev = stock.put("oranges", 20); // prev هي null (مفتاح جديد) int apples = stock.get("apples"); // 60 Integer missing = stock.get("grapes"); // null — المفتاح غير موجود stock.remove("bananas"); // الخريطة الآن: apples=60، oranges=20 System.out.println(stock.size()); // 2 System.out.println(stock.containsKey("apples")); // true System.out.println(stock.containsValue(100)); // false
تجنّب تحويل null إلى نوع بدائي. stock.get("grapes") تُعيد null. تعيينها إلى متغيّر من النوع البدائي int (بدلًا من Integer) يُسبّب NullPointerException. استخدم getOrDefault أو فحص صريح عند احتمال غياب المفتاح.

الوصول الآمن: getOrDefault و computeIfAbsent

getOrDefault(key, fallback) تُعيد القيمة الاحتياطية بدلًا من null عند غياب المفتاح. أما computeIfAbsent فهي الأسلوب الاصطلاحي لبناء قيمة عند الحاجة فقط — مثالية للتجميع:

// عدّاد تكرار الكلمات String[] words = {"cat", "dog", "cat", "bird", "dog", "cat"}; Map<String, Integer> freq = new HashMap<>(); for (String word : words) { freq.put(word, freq.getOrDefault(word, 0) + 1); } // {cat=3, dog=2, bird=1} // تجميع الكلمات حسب الحرف الأوّل باستخدام computeIfAbsent Map<Character, List<String>> byLetter = new HashMap<>(); for (String word : words) { byLetter.computeIfAbsent(word.charAt(0), k -> new ArrayList<>()).add(word); } // {c=[cat, cat, cat], d=[dog, dog], b=[bird]}
computeIfAbsent مختصرة وآمنة للخيوط في ConcurrentHashMap. فضّلها على نمط get-null-check-put عند بناء هياكل مجمّعة أو متداخلة.

تفرّد المفاتيح و equals / hashCode

تستخدم HashMap دالة hashCode() للمفتاح لاختيار دلو (bucket)، ثم equals() لتأكيد الهوية. هذا يعني:

  • كائنان متساويان بـequals() يجب أن يكون لهما نفس hashCode().
  • إذا تجاوزت equals() في فئة مخصصة دون تجاوز hashCode()، ستُنشئ الخريطة مفاتيح مكررة بصمت.
  • يجب ألّا يتغيّر المفتاح بعد إدراجه — تعديل المفتاح يكسر البحث.
// آمن: String و Integer غير قابلَين للتعديل، وhashCode محدَّدة جيدًا. Map<String, String> capitals = new HashMap<>(); capitals.put("France", "Paris"); capitals.put("France", "Lyon"); // يستبدل — "France".equals("France") صحيح System.out.println(capitals.get("France")); // Lyon

التكرار عبر الإدخالات

توجد ثلاث طرق شائعة للتكرار عبر HashMap. الأنظف هي entrySet() التي تعطيك المفتاح والقيمة معًا دون بحث ثانٍ:

Map<String, Integer> population = new HashMap<>(); population.put("Tokyo", 13960000); population.put("Cairo", 9540000); population.put("London", 8982000); // 1. entrySet — الأكثر كفاءة حين تحتاج المفتاح والقيمة معًا for (Map.Entry<String, Integer> entry : population.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); } // 2. keySet — حين تحتاج المفاتيح فقط for (String city : population.keySet()) { System.out.println(city); } // 3. forEach مع lambda (Java 8+) population.forEach((city, pop) -> System.out.printf("%s: %,d%n", city, pop));
ترتيب التكرار غير مضمون. لا تحفظ HashMap ترتيب الإدراج. إن احتجت ترتيبًا متوقّعًا استخدم LinkedHashMap (ترتيب الإدراج) أو TreeMap (مرتّب بالمفتاح) — كلاهما مشمول في الدرس التالي.

الأداء والسعة الابتدائية

داخليًا، تخزّن HashMap الإدخالات في مصفوفة من الدلاء. حين يتجاوز عدد الإدخالات capacity * loadFactor (الافتراضي 0.75)، تُعيد التجزئة — تخصّص مصفوفة أكبر وتوزّع جميع الإدخالات من جديد. إعادة التجزئة تكلف O(n) وهي مكلفة. إن كنت تعرف الحجم التقريبي مسبقًا، مرّره في المنشئ لتجنّب إعادة التجزئة:

// يُتوقّع ~1000 إدخال — تحديد الحجم مسبقًا لتجنّب إعادة التجزئة Map<String, Object> cache = new HashMap<>(1400); // 1000 / 0.75 ≈ 1334 → أعلى تقريبًا

putIfAbsent و merge

طريقتان أخريان تستحقان المعرفة قبل المتابعة:

  • putIfAbsent(key, value) — يُدرج فقط إذا كان المفتاح غائبًا؛ يُعيد القيمة الموجودة في حال وجودها.
  • merge(key, value, remappingFn) — إذا كان المفتاح غائبًا يُدرج القيمة؛ وإن كان موجودًا يطبّق الدالة لدمج القيمة القديمة والجديدة. مفيد جدًا للتجميع.
Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 10); scores.putIfAbsent("Alice", 99); // يُتجاهَل — Alice موجودة بالفعل scores.putIfAbsent("Bob", 20); // يُدرَج // تراكم النقاط بـ merge scores.merge("Alice", 5, Integer::sum); // Alice: 10 + 5 = 15 scores.merge("Carol", 8, Integer::sum); // Carol: 8 (مفتاح جديد) System.out.println(scores); // {Alice=15, Bob=20, Carol=8}

الخلاصة

HashMap هي خزّان المفتاح-القيمة الرئيسي في إطار مجموعات Java. استخدم put/get/remove للعمليات الأساسية، وgetOrDefault وcomputeIfAbsent للوصول الآمن وبناء الهياكل المتداخلة، وentrySet() مع for-each أو lambda forEach للتكرار. تذكّر أن تفرّد المفاتيح يُطبَّق عبر equals() وhashCode() — تجاوزهما دائمًا معًا في فئات المفاتيح المخصصة.