كيف يعمل HashMap في جافا؟  - 1في كثير من الأحيان في المقابلات يطرحون أسئلة مثل " كيف يعمل HashMap في Java؟" "،" ما هي الآلية الداخلية لكيفية عمل الأساليب getفي putHashMap؟ ". سأحاول هنا شرح الوظيفة الداخلية باستخدام مثال بسيط. بدون الخوض في الكثير من النظريات، سنبدأ بمثال حتى تتمكن من فهم أفضل ومن ثم معرفة كيفية عمل الأساليب في Java getأيضًا . putدعونا نأخذ مثالا بسيطا جدا. لدينا فئة Country("بلد" باللغة الإنجليزية)، وسوف نستخدم كائن الفئة Countryكمفتاح، واسم عاصمة هذا البلد كقيمة. فيما يلي مثال لمساعدتنا في فهم كيفية تخزين زوج المفتاح والقيمة في خريطة التجزئة.

1. البلد.جافا

package org.arpit.javapostsforlearning;
public class Country {

 String name;
 long population;

 public Country(String name, long population) {
  super();
  this.name = name;
  this.population = population;
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public long getPopulation() {
  return population;
 }
 public void setPopulation(long population) {
  this.population = population;
 }

 // если длина имени в an objectе Country - четное число,
 // то возвращаем 31(любое случайное число), а если нечетное - 95 (любое случайное число).
 // указанный ниже метод - это не самый лучший способ генерации хеш-codeа,
 // но мы воспользуемся им для более четкого понимания хеш-карт.

 @Override
 public int hashCode() {
  if(this.name.length()%2==0)
   return 31;
  else
   return 95;
 }
 @Override
 public boolean equals(Object obj) {

  Country other = (Country) obj;
   if (name.equalsIgnoreCase((other.name)))
   return true;
  return false;
 }
}
إذا كنت ترغب في فهم ومعرفة المزيد عن الطرق hashcodeوالمتساويات، يمكنك اتباع هذا الرابط .

2. HashMapStructure.java (الفئة الرئيسية)

import java.util.HashMap;
import java.util.Iterator;

public class HashMapStructure {

    /**
     * @author Arpit Mandliya
     */
    public static void main(String[] args) {

        Country india=new Country("India",1000);
        Country japan=new Country("Japan",10000);

        Country france=new Country("France",2000);
        Country russia=new Country("Russia",20000);

        HashMap<country,string> countryCapitalMap=new HashMap<country,string>();
        countryCapitalMap.put(india,"Delhi");
        countryCapitalMap.put(japan,"Tokyo");
        countryCapitalMap.put(france,"Paris");
        countryCapitalMap.put(russia,"Moscow");

        Iterator<country> countryCapitalIter=countryCapitalMap.keySet().iterator();//установите
        //debug-точку на этой строке(23)
        while(countryCapitalIter.hasNext())
        {
            Country countryObj=countryCapitalIter.next();
            String capital=countryCapitalMap.get(countryObj);
            System.out.println(countryObj.getName()+"----"+capital);
            }
        }
}
الآن قم بتعيين نقطة التوقف على السطر 23 وقم بتشغيل run -> debug as-> java application (ملاحظة المترجم - صالحة لـ Eclipse). سيتوقف البرنامج عن التنفيذ في السطر 23، وبعد ذلك انقر بزر الماوس الأيمن على CountryCapitalMap وحدد watch . سيظهر لك جدول كالتالي: كيف يعمل HashMap في جافا؟  - 2وهنا نرى ما يلي:
  1. يوجد مصفوفة Entry[]مكونة من 16 خلية مسماة table;

  2. تقوم هذه المصفوفة بتخزين كائنات الفئة Entry. يحتوي الفصل HashMapعلى فئة داخلية - Entry. ومثيلات هذه الفئة هي أزواج ذات قيمة أساسية. دعونا نلقي نظرة على هيكل الطبقة Entry:

  3. static class Entry implements Map.Entry
            {
                    final K key;
                    V value;
                    Entry next;
                    final int hash;
                    ...//продолжение codeа
            }
  4. في كل مرة نحاول إنشاء زوج من المفاتيح والقيمة في خريطة التجزئة، سيتم إنشاء كائن فئة لهذا الزوج Entryوسيتم تخزينه في الجدول أعلاه Entry[]. والآن يجب أن تتساءل أين سيتم كتابة هذا الكائن بالضبط في هذا الجدول (في أي خلية). بالنسبة للمفتاح في زوج المفتاح والقيمة، يتم حساب رمز التجزئة باستخدام hashcode(). ويستخدم رمز التجزئة هذا لحساب رقم خلية الجدول Entry[]؛

  5. الآن إذا نظرت إلى الخلية 10 من الجدول، فسوف ترى كائن فئة Entryاسمه HashMap$Entry؛

  6. لقد أضفنا 4 أزواج من القيمة الرئيسية، ولكن لا يوجد سوى 2 في المصفوفة!!! وذلك لأنه إذا كان هناك كائنين لهما نفس رمز التجزئة، فسيتم تخزينهما في نفس الخلية. ولكن كيف؟ سيتم تخزين الكائنات كقائمة مرتبطة ( LinkedList).
إليك كيفية حساب رمز التجزئة لأزواج القيمة الرئيسية لدينا.
Hashcode for Japan = 95 так How длина слова Japan имеет нечетное количество букв.
Hashcode for India = 95 так How длина слова India имеет нечетное количество букв.
HashCode for Russia = 31 так How длина слова Russia имеет четное количество букв.
HashCode for France = 31 так How длина слова France имеет четное количество букв.
يشرح الشكل التالي فكرة القائمة المرتبطة: كيف يعمل HashMap في جافا؟  - 3الآن بعد أن فهمت بالفعل بنية خرائط التجزئة، دعنا ننتقل إلى الطرق putوالطرق get.

يضع:

دعونا نرى كيف يتم استخدام هذه الطريقة:
/**
  * Метод связывает указанное meaning с указанным ключом в данной хэш-карте. Если
  * карта до этого уже содержала некоторое meaning, соответствующее этому ключу,
  * то старое meaning заменяется на указанное.
  * @param key
  *            ключ, с которым связывается указанное meaning
  * @param value
  *            meaning, связываемое с указанным ключом
  * @возвращает meaning связанное с <tt>ключом</tt>, or <tt>null</tt>,
  *         если ниHowое meaning не соответствует <tt>ключу</tt>. ( Возврат <tt>null</tt>
  *         может так же говорить о том, что в карте заведомо <tt>null</tt> был связан с
  *         <tt>ключом</tt>.)
  */
 public V put(K key, V value) {
  if (key == null)
   return putForNullKey(value);
  int hash = hash(key.hashCode());
  int i = indexFor(hash, table.length);
  for (Entry<k , V> e = table[i]; e != null; e = e.next) {
   Object k;
   if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
   }
  }

  modCount++;
  addEntry(hash, key, value, i);
  return null;
 }
الآن دعونا نحاول فهم هذا الرمز خطوة بخطوة:
  1. نتحقق من الكائن keyمن أجل المساواة null. keyإذا كان الأمر كذلك، فسيتم تخزين الكائن في الموقع table[0]لأن رمز التجزئة الخاص به nullهو دائمًا 0؛

  2. keyبعد ذلك، نسمي طريقة الكائن hashcode()، والتي ستحسب رمز التجزئة الخاص به. يتم استخدام رمز التجزئة هذا لتحديد خلية الصفيف حيث سيتم تخزين كائن الفئة Entry. في بعض الأحيان يحدث أن هذه الوظيفة hashcodeلم تتم كتابتها بمهارة كبيرة، لذلك قام مطورو JDK بإنشاء وظيفة مختلفة - hash()والتي تأخذ رمز التجزئة المحسوب مسبقًا كوسيطة. إذا كنت مهتمًا بالقراءة عن هذه الوظيفة بمزيد من التفاصيل، فيمكنك اتباع الرابط ؛

  3. indexFor(hash,table.length)يتم استخدامه لتحديد خلية معينة في المصفوفة tableالتي سيتم تعريف كائن فئة ليتم تخزينها فيها Entry؛

  4. كما رأينا في مثالنا، إذا keyكان هناك كائنان لهما نفس رمز التجزئة (يُعرف هذا الموقف بالتصادم)، فسيتم تخزينهما في شكل قائمة مرتبطة. لذلك، في هذه المرحلة نكرر قائمتنا:

    • إذا كانت الخلية المحسوبة حديثًا فارغة، Entryفسيتم حفظ كائن الفئة مباشرة في هذه الخلية؛

    • إذا كانت هذه الخلية تحتوي بالفعل على كائن ما، فسيتم التكرار إلى العنصر الذي nextيساوي حقله null. بعد ذلك، Entryيصبح كائن الفئة الخاص بنا هو التالي في القائمة؛

    • ماذا لو أضفنا نفس الكائن keyمرة أخرى؟ منطقيا، يجب أن تحل محل القيمة القديمة. نعم، سيكون الأمر كذلك. أثناء التكرار، ستتم مقارنة المفاتيح باستخدام الطريقة equals()( key.equals(k)). إذا كانت النتيجة صحيحة، فسيتم استبدال القيمة القديمة بقيمة الكائن الحالي Entry.

يحصل:

الآن دعونا نلقي نظرة على تطبيق هذه الطريقة يحصل
/**
  * returns meaning, которое соответствует указанному ключу, or {@code null}, если
  * данная карта не содержит пары с указанным ключом.
  *
  *
  * <p>
  * Более точно, если в данной карте содержится такой ключ {@code k}
  * с соответствующим ему meaningм {@code v}, что {@code (key==null ? k==null : key.equals(k))},
  * то метод возвращает {@code v}; в противном случае возвращается {@code null}.
  * (может быть не более одной такой пары)
  *
  * </p><p>
  * Возвращенное meaning {@code null} не <i>обязательно</i> говорит о том, что
  * в карте нет пары с таким указанным ключом; а возможно, что в карте однозначно
  * указано соответствие этого ключа со meaningм {@code null}.
  * Можно воспользоваться операцией {@link #containsKey containsKey}, чтобы
  * отличить эти два случая
  * @see #put(Object, Object)
  */
 public V get(Object key) {
  if (key == null)
   return getForNullKey();
  int hash = hash(key.hashCode());
  for (Entry<k , V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
   Object k;
   if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
    return e.value;
  }
  return null;
 }
الآن بعد أن فهمت كيفية عمل طريقة الوضع في خرائط التجزئة، أصبح فهم كيفية عمل طريقة الوضع أمرًا getبسيطًا للغاية. عند تمرير أي مفتاح إلى طريقة للحصول على قيمة من خريطة التجزئة:
  1. شيء يتم اختبار Ekey من أجل المساواة null. إذا كان الأمر كذلك، فسيتم إرجاع قيمة الكائن المخزن في الخلية table[0]؛

  2. يحتوي الكائن الرئيسي على طريقة تسمى hashcode()تحسب رمز التجزئة؛

  3. indexFor(hash,table.length)تستخدم لتحديد خلية صفيف محددة tableيمكن أخذ كائن فئة منها Entry؛

  4. بعد تلقي رقم خلية المصفوفة، tableسيتم تكرار القائمة ومقارنة المفاتيح باستخدام الطريقة equals(). إذا كانت النتيجة صحيحة، فسيتم إرجاع قيمة الكائن Entry، وإلا - null.

أشياء للذكرى:

  • تحتوي الفئة HashMapعلى فئة داخلية Entryتخزن أزواج القيمة الرئيسية؛

  • يتم تخزين كائنات الفئة Entryفي مصفوفة Entry[ ]تسمى table;

  • تسمى خلية المصفوفة بالدلو وتقوم بتخزين العنصر الأول من القائمة المرتبطة؛

  • يتم استخدام طريقة hashcode()الكائن keyللعثور على مجموعة كائنات الفئة هذه Entry؛

  • إذا كان لمفتاحي كائنين نفس رمز التجزئة، فسيتم تخزينهما في نفس مجموعة المصفوفة table؛

  • يتم استخدام طريقة equals()الكائن keyلتأكيد تفرده؛

  • لا يتم استخدام الأساليب equals()والكائنات على الإطلاق.hashcode()value

مصدر