HashMap vs Hashtable في جافا: من ينتصر؟ نتعمق تحت الغطاء
\n\nإذا سبق لك أن تساءلت: "ما الفرق بين HashMap و Hashtable؟" — فأنت لست وحدك. هذا أحد أكثر الأسئلة شيوعًا على Stack Overflow، وليس بدون سبب. كلا الفئتين تخزنان البيانات بتنسيق "مفتاح — قيمة"، وكلاهما يستخدم التجزئة، لكنهما يعيشان في عالمين مختلفين. الأولى سريعة وعصرية، والثانية قديمة وموثوقة ولكنها بطيئة بعض الشيء. دعونا نلقي نظرة تحت الغطاء ونفهم ما الأمر.
\n\nفي هذه المقالة، لن نقوم فقط بسرد الاختلافات. بل سنفهم كيف تعمل الآلية الداخلية، ولماذا تسمح إحدى الفئتين بـ null بينما الأخرى لا، ومتى يجب استخدام ConcurrentHashMap بدلاً من كليهما.
نبذة تاريخية سريعة
\n\nHashtable ظهر في جافا منذ الإصدار 1.0. كانت واحدة من أولى فئات المجموعات، وكانت متزامنة منذ البداية. في تلك الأيام، كان تعدد الخيوط نادرًا، لكن مطوري جافا قرروا توخي الحذر. لاحقًا، في جافا 2 (1.2)، ظهرت مجموعات جديدة من إطار عمل المجموعات Collections Framework، ومن بينها HashMap. كانت غير متزامنة، لكنها أسرع بكثير. منذ ذلك الحين، تعتبر Hashtable قديمة (legacy)، على الرغم من عدم إزالتها من اللغة.
\n\nالفرق الرئيسي: المزامنة والخيوط
\n\nالفرق الأول والأهم هو المزامنة. طرق Hashtable (مثل put() و get() و remove()) موسومة بالكلمة المفتاحية synchronized. هذا يعني أن خيطين لا يمكنهما تنفيذ نفس الطريقة في نفس الوقت على كائن Hashtable واحد. يبدو هذا كميزة، لكنه في الواقع يتحول إلى عنق زجاجة.
// Hashtable — جميع الطرق متزامنة\nHashtable<String, String> table = new Hashtable<>();\ntable.put("key1", "value1"); // آمن للخيوط، لكنه بطيء في ظل المنافسة العالية\n\n// HashMap — غير متزامن\nHashMap<String, String> map = new HashMap<>();\nmap.put("key1", "value1"); // سريع، لكنه غير آمن للخيوط\n\n\nإذا كنت تعمل في تطبيق أحادي الخيط (وهو الغالب)، فإن HashMap سيعمل بشكل أسرع، لأنه لا يحتاج إلى إضاعة الوقت في الحصول على الأقفال وتحريرها. أما إذا كنت بحاجة إلى أمان الخيوط، فمن الأفضل عدم استخدام Hashtable، بل استخدام ConcurrentHashMap (الذي ظهر في جافا 5). يستخدم هذا الأخير قفلًا أكثر دقة (قطاعات أو أقفال على مستوى الدلاء) ويعمل بشكل أسرع بكثير في بيئة متعددة الخيوط.
Null، null، و null مرة أخرى
\n\nالفرق المهم الثاني هو التعامل مع null. يسمح HashMap باستخدام null كمفتاح (مرة واحدة فقط) وكقيم (أي عدد من المرات). بينما سيرمي Hashtable استثناء NullPointerException إذا حاولت تمرير null.
HashMap<String, String> map = new HashMap<>();\nmap.put(null, "nullKey"); // مسموح\nmap.put("key", null); // مسموح\nSystem.out.println(map.get(null)); // "nullKey"\n\nHashtable<String, String> table = new Hashtable<>();\n// table.put(null, "value"); // NullPointerException!\n// table.put("key", null); // NullPointerException!\n\n\nلماذا هذا؟ الأمر كله يتعلق بكيفية عمل التجزئة. في HashMap، يتم استخدام رمز تجزئة خاص بقيمة 0 للمفتاح null، ويتم وضعه في الدلو الصفري (bucket). بينما يستخدم Hashtable آلية قديمة، حيث يؤدي استدعاء hashCode() على null إلى خطأ. بالإضافة إلى ذلك، لا يعتمد Hashtable على القيم null، لأن ذلك سينتهك منطق العمليات المتزامنة.
البنية الداخلية: كيف يعمل جدول التجزئة تحت الغطاء
\n\nلفهم الفرق بشكل أعمق، دعونا نلقي نظرة داخلية. يعتمد كل من HashMap و Hashtable على جدول التجزئة. وهو عبارة عن مصفوفة من "الدلاء"، حيث كل دلو عبارة عن قائمة مرتبطة (أو شجرة، بدءًا من جافا 8). عند إضافة