تعتبر ال HashMap واحدة من أكثر الأنظمة البيانية استخدامًا في الجافا وهي معروفة بالكفاءة. تخزين البيانات في ال HashMap يحدث بالأيام من المفاتيح المفتاحة.

في هذه المقالة، سأقوم بتقديم ال HashMap في الجافا. سنبتاع فيها العمليات الشائعة لل HashMap ومن ثم سندخل كيفية عملها من الداخل. ستحصل على فهم للمعاملة ال哈希ية وكيفية حساب المؤشرات. وأخيرًا، سننظر إلى تعقيدات الوقت للعمليات ونلمس على السلوك في بيئة متعددة المراتب.

ما هو ال HashMap في الجافا؟

تنفيذ ال HashMap تنفيذ ال Map التي تقع ضمن الأنظمة الجمعية الجافاية. إنها مبنية على مبدأ التحويل.

التحويل هو تقنية تحول معيار دخول لا محدد في ناتج ثابت بواسطة معاملة تحويلية. الناتج المنتج يطلق عليه معاملة التحويل ويمثل قيمة 整数 في الجافا. يستخدم معاملات التحويل للبحث والتخزين الفعالين في ال HashMap.

العمليات الشائعة

وكما تحدثنا فوق ، تخزين البيانات في ال HashMap يحدث بالأيام من المفاتيح المفتاحة.

أدنا بعض العمليات الشائعة المدعومة من خلال ال HashMap. دعونا نفهم ما تفعل هذه الطرق ببعض الأدوات البسيطة:

التكامل

  • هذه الطريقة تضيف مفاتيح جديدة لل HashMap.

  • ترتيب الإدراج لأزواج المفتاح والقيمة لا يُحتفظ به.

  • أثناء الإدراج، إذا كان المفتاح موجودًا بالفعل، سيتم استبدال القيمة الحالية بالقيمة الجديدة التي يتم تمريرها.

  • يمكنك إدراج مفتاح واحد فقط من نوع القيمة null في HashMap، ولكن يمكنك امتلاك قيم null متعددة.

توقيع الأسلوب لهذه العملية معروض أدناه، تليه مثال:

public V put(K key, V value)
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);

في الشيفرة أعلاه، لدينا مثال على HashMap حيث نقوم بإضافة مفتاح من نوع String وقيمة من نوع Integer.

استرجاع:

  • يستخرج القيمة المرتبطة بالمفتاح المعطى.

  • يعيد null إذا لم يكن المفتاح موجودًا في HashMap.

توقيع الأسلوب لهذه العملية معروض أدناه، تليه مثال:

public V get(Object key)
Integer value = map.get("apple"); // يُرجع 1

في الشيفرة أعلاه، نقوم باسترجاع القيمة المرتبطة بالمفتاح apple.

تشمل العمليات الشائعة الأخرى:

  • remove: يقوم بإزالة زوج المفتاح والقيمة للمفتاح المحدد. يُعيد null إذا لم يتم العثور على المفتاح.

  • containsKey: يقوم بالتحقق مما إذا كان مفتاح محدد موجودًا في HashMap.

  • containsValue: يقوم بالتحقق مما إذا كانت القيمة المحددة موجودة في HashMap.

الهيكل الداخلي لـ HashMap

داخليًا، يستخدم HashMap مصفوفة من القوارير أو الحاويات. تكون كل حاوية قائمة مرتبطة من نوع Node، والتي تُستخدم لتمثيل زوج المفتاح والقيمة في HashMap.

static class Node<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;

    Node(int hash, K key, V value, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

أعلاه، يمكنك رؤية هيكل فئة Node التي تُستخدم لتخزين أزواج المفتاح والقيمة في HashMap.

تحتوي فئة Node على الحقول التالية:

  • hash: يشير إلى hashCode للمفتاح.

  • key: يشير إلى المفتاح لزوج المفتاح والقيمة.

  • value: يشير إلى القيمة المرتبطة بالمفتاح.

  • 下一个: يؤدي كمرجع إلى النقطة التالية.

تعتمد ال HashMap أساسًا على تطبيق جدول ال哈希، وأداءها يعتمد على معاييرين رئيسيين: الكapacity الأولي ونسبة التعبئة. ال original javadocs لسنة الجدول الهاش تعرف هذين المعاييرين بالتالي:

  • الكapacity هو عدد الخلايا في الجدول الهاش، والكapacity الأولي ببساطة هي الكapacity التي يتم إنشاؤها عند إنشاء الجدول الهاش.

  • المؤشر الكامل هو مؤشر على تعبئة الجدول الهاش قبل أن يتم تعزيز كapacityهؤلاء القدرات تلقائيًا.

دعونا نحاول فهم كيفية عمليات التعامل الأساسيين، put و get، في HashMap.

ال函数 الهاش

أثناء إدراج (put) زوج مفاتيح مفاتيح، يقوم HashMap أولاً بحساب رمز المفتاح. يقوم ما بعدها مع المادة بحساب رقم 整数 للمفتاح. يمكن للأقسام أن تستخدم مethod hashCode من قاعدة Object أو تغير هذا المethod وتوفر تطبيق خاص به. (قرأ عن قراءة العقد الرمزي هنا). يتم بعدها تصادم الرمز بأعلى 16 بت (h >>> 16) لتحقيق توزيع أكثر متجانسًا.

التصادم النقطي البيني هو عملية تقارن بين عنصرين من البتات، وينتج 1 إذا كانت البتات مختلفة و 0 إذا كانت متطابقة. في هذا السياق، أداء عملية التصادم النقطي البيني بين رمز المفتاح وأعلى 16 بته (من خلال استخدام مؤشر التحول الأيمن الغير معتمد >>>) يساعد على تركيب البتات، مما يقود إلى توزيع رمز المفتاح المتجانس أكثر.

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

في الأعلى، يمكنك رؤية مETHOD المصادفة الثابت للقاعدة HashMap.

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

حساب المؤشر

بمجرد إنشاء رمز المفتاح للمفتاح، يحسب HashMap مؤشر داخل الأرrays الخاصة بالأكياد لتحدد أين سيتم تخزين زوج المفتاح والقيمة. يتم هذا بواسطة عملية الواحد والصفر، التي هي طري

int index = (n - 1) & hash;

تحيز هنا نحن نحسب الفهرس حيث يكون n الطول الى الصندوق المصفوفة.

بمجرد حساب الفهرس، يتم تخزين المفتاح في ذلك الفهرس في مصفوفة الصندوق. ومع ذلك، إذا كان للمفاتيح المتعددة نفس الفهرس، يتسبب في تصادم. في هذه الحالة، يتعامل HashMap معها بأحد الطرقين:

  • التسلسل/الربط: كل صندوق في المصفوفة هو قائمة من العقد الرابطة. إذا كان هناك مفتاح موجود بفهرس معين وتم إجراء تصادم لمفتاح آخر إلى نفس الفهرس، يتم إلحاقه بالقائمة.

  • تشجيع الشجرة: إذا تجاوز عدد العقد العتبة الحدودية، يتم تحويل القائمة الرابطة إلى شجرة (وقد تم تقديم هذا في جافا 8).

static final int TREEIFY_THRESHOLD = 8;

هذه هي العتبة التي تحدد تشجيع الشجرة.

ولهذا، من المهم أن يكون لديك وظيفة تجزئة جيدة توزع المفاتيح بشكل متساوٍ عبر الصناديق وتقلل من فرصة التصادمات.

العمليات الاسترجاعية (get) والحذفية (remove) تعمل بشكل مماثل للعملية الإدراجية (put). وهنا كيف:

  • استرجاع (get): يحسب رمز الهاش باستخدام وظيفة الهاش -> يحسب الفهرس باستخدام رمز الهاش -> يتنقل عبر القائمة الرابطة أو الشجرة للعثور على العقد بالمفتاح المطابق.

  • الحذف (remove): يحسب رمز التجزئة باستخدام وظيفة التجزئة -> يحسب الفهرس باستخدام رمز التجزئة -> يزيل العقدة من القائمة المرتبطة أو الشجرة.

تعقيد الوقت

العمليات الأساسية لـ HashMap ، مثل put ، get ، و remove ، عادةً ما توفر أداءً زمنيًا ثابتًا بتعقيد O(1) ، بشرط أن تكون المفاتيح موزعة بشكل متساوٍ. في الحالات التي يوجد فيها توزيع ضعيف للمفاتيح وتحدث العديد من التصادمات ، قد يتدهور هذا الأداء إلى تعقيد زمني خطي بتعقيد O(n).

تحت تحويل الشجرة ، حيث يتم تحويل سلاسل طويلة من التصادمات إلى شجيرات متوازنة ، يمكن تحسين عمليات البحث لتعقيد زمني لوغاريتمي أكثر كفاءة بتعقيد O(log n).

المزامنة

تنفيذ HashMap غير متزامن. إذا قامت عدة خيوط بالوصول إلى مثيل HashMap بشكل متزامن وقام أي من الخيوط بتنفيذ تعديل هيكلي (مثل إضافة أو إزالة تعيين قيمة مفتاح) على الخريطة ، فإن ذلك يؤدي إلى حدوث استثناء ConcurrentModificationException.

لمنع هذا، يمكنك إنشاء مصادر آمنة بواسطة الطريقة Collections.synchronizedMap.

الخلاصة

في الختام، فهي من المهم جداً للمطورين فهم تشغيل داخلية ال HashMap للقيام بخيارات معلومة. ومعرفة كيفية ترتيب المفاتيح، وكيف يحدث التقاطع، وكيف يمكن تجنبه ستساعدك على استخدام ال HashMap بالكامل وبالفعالية.