static int indexFor(int h, int length) {
return h & (length-1);
}
Кіріс ретінде ол жұмыс нәтижесінде алынған хэш-codeты hashCode()
және ішкі массивтің ұзындығын (ұяшықтардың саны) алды. Және ол нәтижені қайтарды «хэш codeы» -> биттік «ЖӘНЕ» -> (массив ұзындығы – 1). Класс HashMap
сыныптан мұра алады AbstractMap
және келесі интерфейстерді жүзеге асырады: Map
, Cloneable
, Serializable
. .әдісі Java тіліндегі хэш функциясына жауап береді hashCode()
. Әдепкі іске асыру сәйкестендіру хэш codeыhashCode()
деп аталатын мәнді қайтарады . Сынып қайта анықталса да , сіз әрқашан қоңырау шалу арқылы нысанның идентификатор хэшін ала аласыз . OpenJDK-дегі әдепкі енгізудің жад мекенжайына ешқандай қатысы жоқ, кейде сенетіндей. Толығырақ мына жерден: habr HashMap бағдарламасында хэш кестесі жеке байланыстырылған тізімдердің массивіне (немесе, дәлірек айтқанда, динамикалық, кесте кеңеюі мүмкін) негізделген. Негізінде, біз әдіс нәтижесінде кілттің хэш codeын аламыз , ол кейін өзгертіледі (кейінірек қарастырамыз) және ішкі қосымша әдісті қолдана отырып, алынған мәндер қажетті ұяшықтарға таратылады. Массив элементтері (ұяшықтар) жеке түйіндерді сақтау үшін қолданылатын шелек деп те аталады. Шелектердің әрқайсысы жинақ (тізім немесе ағаш). Түйін кірістірілген класстың (немесе ағаш құрылымындағы) нысаны болып табылады. Шындығында, массив ұяшығының ішінде басқа класстың жүзеге асуының негізінде жатқан жалғыз байланыстырылған тізім немесе қызыл-қара ағаш бар . hashCode()
System.identityHashCode(Object o)
hashCode()
hashCode()
Node
TreeNode
LinkedList
TreeMap
HashMap
ішінде келесі өрістері бар кірістірілген класс :
final int hash
— кілтті хэштеу нәтижесінде алатын ағымдағы элементтің хэші;final K key
— ағымдағы элементтің кілті. Бұл жерде әдісте бірінші нысан ретінде көрсеткен нәрсе жазыладыput()
;V value
— ағымдағы элементтің мәні. Ал әдісте екінші нысан ретінде көрсеткеніңіз осында жазылғанput()
;Node < K, V> next
— сол себеттегі келесі түйінге сілтеме. Тізім қосылған, сондықтан егер бар болса, келесі түйінге емес сілтеме қажет.
transient Node < K, V> [] table
– түйіндер түріндегі кілт-мән жұптарын сақтауға арналған массив негізінде іске асырылған хэш кестесінің өзі. Бұл жерде түйіндеріміз сақталады;transient int size
— кілт-мән жұптарының саны;int threshold
— элементтердің максималды саны, оған жеткенде хэш кестесінің өлшемі екі есе өседі. Формула арқылы есептелген (сыйымдылық * жүктемеФактор);final float loadFactor
— бұл параметр ағымдағы хэш-кесте жаңа хэш кестесін құру үшін қандай жүктеме дәрежесінде қажет екенін анықтайды, яғни. хэш-кесте 75% толғаннан кейін жаңа хэш-кесте жасалады және оған ағымдағы элементтер жылжытылады (бұл қымбат операция, өйткені барлық элементтерді қайта өңдеу керек);transient Set< Map.Entry< K,V>> entrySet
- кэштелген барentrySet()
, онымен біз қайталай аламызHashMap
.
static final int DEFAULT_INITIAL_CAPACITY= 1 << 4
— әдепкі хэш кестесінің сыйымдылығы (16);static final int MAXIMUM_CAPACITY = 1 << 30
— хэш-кестенің максималды мүмкін болатын сыйымдылығы (шамамен 1 млрд);static final float DEFAULT_LOAD_FACTOR = 0.75f
— әдепкі жүктеме коэффициенті;static final int TREEIFY_THRESHOLD = 8
бір шелектегі элементтер санының «табалдырығы» болып табылады, оған жеткенде ішкі байланыстырылған тізім ағаш құрылымына (қызыл-қара ағаш) түрленеді.static final int UNTREEIFY_THRESHOLD = 6
— егер бір себеттегі элементтер саны 6-ға дейін азайса, онда ағаштан байланыстырылған тізімге кері көшу орын алады;static final int MIN_TREEIFY_CAPACITY = 64
— ағаш құрылымына өту мүмкін болатын хэш-кестенің минималды сыйымдылығы (шелектердің саны). Анау. Егер хэш кестесінде кемінде 64 шелек болса және бір шелекте 8 немесе одан да көп элементтер болса, онда ағаш құрылымына көшу орын алады.
public HashMap()
— әдепкі бойынша хэш-дисплейді жасайды: көлемі бойынша(capacity) = 16
және жүктеме коэффициентімен(load factor) = 0.75
;public HashMap(Map< ? extends K, ? extends V> m)
— басқа картаның элементтерін орналастыру үшін жеткілікті бастапқы сыйымдылығы бар басқа берілген картаның элементтерімен инициализацияланған хэш-карталауды жасайды;public HashMap(int initialCapacity)
— берілген бастапқы сыйымдылығы бар хэш картасын жасайды. HashMap дұрыс және дұрыс жұмыс істеуі үшін ішкі массивтің өлшемі екінің дәрежесі болуы керек (яғни 16, 64, 128 және т.б.);public HashMap(int initialCapacity, float loadFactor)
— көрсетілген параметрлері бар хэш картасын жасайды: бастапқы сыйымдылық және жүктеме коэффициенті.
Map
және классты AbstractMap
өзінің әдістерін қоспай кеңейтеді. Хэш картасы оның элементтерінің ретіне кепілдік бермейді. Демек, элементтердің хэш картасына енгізілу реті олардың итератор арқылы шығарылатын реті болуы міндетті емес. Нысандарды қосу Кілт-мән жұбын қосу арқылы орындалады put()
. Объектіні қосу кезінде орындалатын қадамдарды қарастырайық:
-
Енгізілген нысан кілтінің хэш мәні есептеледі. Негізгі хэш біз білетін негізгі әдіске
static final int hash(Object key)
қол жеткізетін әдіс арқылы есептеледіhashCode()
. Бұл әрекетті орындау үшін қайта анықталған әдісhashCode()
немесе оның әдепкі орындалуы пайдаланылады. Әдістегі қосымша түрлендірулерhash()
:static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
Почему бы просто не вычислить с помощью
hashCode()
? Это сделано из-за того, чтоhashCode()
можно реализовать так, что только нижние битыint
'a будут заполнены. Например, дляInteger
,Float
– если мы в HashMap кладем маленькие значения, то у них и биты хеш-codeов будут заполнены только нижние. В таком случае ключи в HashMap будут иметь тенденцию скапливаться в нижних ячейках, а верхние будут оставаться пустыми, что не очень эффективно. На то, в Howой бакет попадёт новая запись, влияют только младшие биты хеша. Поэтому и придумали различными манипуляциями подмешивать старшие биты хеша в младшие, чтобы улучшить распределение по бакетам (чтобы старшие биты родного хеша an object начали вносить коррективы в то, в Howой бакет попадёт an object) и, How следствие, производительность. Потому и придумана дополнительная функцияhash
внутри HashMap. -
Вычисляем индекс бакета (ячейки массива), в который будет добавлен наш элемент:
i = (n - 1) & hash
где n – длина массива.
-
Создается an object Node.
-
Теперь, зная индекс в массиве, мы получаем список (цепочку) элементов, привязанных к этой ячейке. Если в бакете пусто, тогда просто размещаем в нем элемент. Иначе хэш и ключ нового element поочередно сравниваются с хешами и ключами элементов из списка и, при совпадении этих параметров, meaning element перезаписывается. Если совпадений не найдено, элемент добавляется в конец списка.
Теперь к очень подробному примеру.
-
Создадим an object класса HashMap:
HashMap < String, Integer> map = new HashMap<>();
-
С помощью метода
put()
добавим в хеш-отображение первую пару «ключ-meaning»:map.put("KING", 100);
В этот момент внутри вызывается метод
putVal()
. -
С помощью хеш-функции, роль которой играет метод hash, вычисляется хеш-code ключа, внутри которого предварительно вычисляется хеш-code с помощью метода hashCode() (в данном случае класса String), в результате чего мы получаем его «предварительное meaning» – 2306967. Может проверить в IDEA с помощью
System.out.println("KING".hashCode());
Полученный хеш-code модифицируется по формуле:
(хеш-code) ^ (хеш-code>>> 16)
, и в результате получаем окончательный хеш-code – 2306996. -
Проверяем таблицу на «пустоту»:
if ((tab = table) == null || (n = tab.length) == 0)
где
[] tab
— сама хеш-table: ссылаем tab и table (напомню, что это поле класса HashMap, которое хранит массив для наших элементов) на один и тот же массив, а int n – дополнительная переменная-счетчик.Так How проверка вернёт true (потому что массив для таблицы не был создан с помощью оператора new в конструкторе), будет вызван метод
resize()
, который собственно и создаст таблицу размером на 16 элементов. Да-да, конструкторы класса ниHowой таблицы не создают. Вместо этого всегда происходит вызов методаresize()
при первом добавлении element. Длина созданной таблицы (считай длина массива) будет записана в переменнуюn – n = (tab = resize()).length
, которая в дальнейшем используется для вычисления бакета. -
Одновременно вычисляем индекс бакета, куда будет помещен наш an object, и проверяем, а есть ли уже в нем элементы. Вычисляем:
i = (n - 1) & hash i = (16 - 1) & 2306996 i = 4
проверяем:
if ((p = tab[i = (n - 1) & hash]) == null)
-
Так How в результате проверки мы получим true (в бакете элементов нет), будет сгенерирован an object Node со следующими полями:
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
Наш сформированный an object Node будет добавлен в бакет под индексом [4]:
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
— это метод, который просто возвращает an object класса Node. -
После добавления будет произведена проверка не превышает ли текущее количество элементов параметр
threshold
:if (++size > threshold) resize();
Если превышает, тогда будет вызван метод
resize()
для увеличения размера хеш-таблицы.На этом метод
putVal()
(соответственно иput()
) завершит свою работу.Графически полученный результат изобразить можно так:
Так в общем случае выглядит добавление Node (пара «ключ-meaning») в хеш-таблицу, если нужный бакет оказывается пуст. Теперь попробуем добавить такой элемент, который приведет к коллизии (когда в одном бакете оказывается более одного element).
-
- сыртқы тізбектеу немесе тізбектеу әдісі (HashMap-те енгізілген) - яғни. ұяшықта шын мәнінде тізім (тізбек) бар. Және тізімде бірнеше мәндер болуы мүмкін (міндетті түрде бірдей хэш-codeпен емес).
- сызықтық зондтау немесе ашық addressтеу әдісі (IdentityHashMap бағдарламасында енгізілген) - хэш функциясымен көрсетілген ұяшықтан кейінгі бірінші бос ұяшықты іздеуден тұрады;
-
Әдісті пайдалана отырып,
put()
хэшті салыстыруға басқа кілт-мән жұбын қосамыз:map.put("BLAKE", 10);
-
Біз «алдын ала хэшті» есептейміз – 63281361. Біз оны өзгертеміз және нәтижесінде соңғы хэш codeын аламыз – 63281940.
-
«Бостығын» бірінші тексеру енді жалған мәнін қайтаратындықтан (кесте жасаудың қажеті жоқ), біз an objectіміз орналасатын шелек индексін дереу есептейміз:
i = (n - 1) & hash i = (16 - 1) & 63281940 i = 4
-
Көрсетілген индекстегі шелек ондағы элементтердің бар-жоғы үшін тексеріледі және
if ((p = tab[i = (n - 1) & hash]) == null)
бұл жағдайда шарт орындалмағандықтан (шелекте элемент бар), содан кейін біз блокқа барамызelse
. -
Ең алдымен, қосылатын элементті шелек ішіндегі байланыстырылған тізімнің бірінші элементімен салыстырамыз:
(p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
Тексеру кезінде алдымен кілт хэштері салыстырылады. Егер бұл «бөлік»
(p.hash == hash)
жалған мәнін қайтарса, онда қалған шарт еленбейді (&&
) өйткені нысандардың басқаша болуына кепілдік беріледі. Әйтпесе, кілттер сілтеме бойынша (==) салыстырылады және теңсіздік жағдайында кілттер әдісі арқылы салыстырыладыequals()
. Салыстырулар өнімділік үшін осы ретпен жасалады. Егер барлық үш өрнек шын мәнін қайтарса, бұл кілттердің тең екендігін және кейінірек кілт мәнін қайта жазу үшін жаңа элемент қосымша айнымалыға жазылатынын білдіреді:if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
Кілттерді салыстыру нәтижесінде біз бірінші кезеңде жалған аламыз (әртүрлі хэштер).
-
Біз ( ) шартын елемейміз
p instanceof TreeNode
, өйткені бұл кезеңде шелектегі құрылым ағаш тәрізді емес. -
Содан кейін біз циклге кіреміз
for
, онда бір шелек ішінде келесі элементке көрсеткіш үшін элементтерді тексеремізnext
және егер ол нөл болса (бұл тізімдегі элемент соңғы және жалғыз екенін білдіреді), біз жаңа Түйін қосамыз. тізімнің соңына дейін элемент:if ((e = p.next) == null){ p.next = newNode(hash, key, value, null) ... };
Сіз сұрақ қоюыңыз мүмкін, негізгі теңдік тексеруі қайда? Біз жай ғана алға шығып, жаңа элемент қоса алмаймыз. Сондықтан ол (5) тармақта алдын ала орындалды. Осының арқасында біз осы элементтің көрсеткішін жай ғана тексере аламыз және ол қазір нөл болғандықтан, тізімде бір ғана элемент бар деп қорытынды жасауға болады. Біз олардың кілттерін тексеріп қойғандықтан, жаңа элементті қауіпсіз қоса аламыз.
Егер бірінші итерация кезінде көрсеткіш нөл болмаса, бұл тізімде кемінде екі элемент бар екенін көрсетеді, сондықтан бұл жағдайда келесі шартқа көшеміз және қосылатын элементтің кілтін барлық пернелермен салыстырамыз. тізімдегі элементтер (бесінші абзацта сипатталған тәртіппен).
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
Егер кілттерді салыстыру кезінде сәйкестік табылса, жаңа элемент қосымша айнымалыға жазылады, осылайша оны кейінірек кілттің мәнін қайта жазу үшін пайдалануға болады.
Екінші элементті қосқаннан кейін, біздің HashMap нысаны графикалық түрде келесідей болады:
Нәтижесінде, соқтығыс кезінде элементтерді қосу (бір шелек ішінде) келесідей болады:
- Біз екі кілттің де бірдей екенін
hashCode()
және әдістерін қолданып тексереміз .equals()
- пернелер бірдей болса, ағымдағы мәнді жаңасымен ауыстырыңыз.
- әйтпесе, ағымдағы нысандағы келесі нысанға сілтемені көрсете отырып, «байланыстырылған тізім» деректер құрылымын пайдаланып жаңа және ескі нысандарды қосыңыз және екеуін де қажетті индекс астында сақтаңыз; немесе ағаш құрылымына ауысыңыз
- Біз екі кілттің де бірдей екенін
-
Әрбір итерациядан кейін (жаңа элементті қосу) for циклі шелектегі элементтердің санына жауап беретін есептегішті арттырады:
for (int binCount = 0; ; ++binCount)
Олардың саны 7-ге тең немесе одан көп болғанша:
binCount >= TREEIFY_THRESHOLD – 1
treeifyBin()treeify()
Бұл жағдайда ағаш құрылымын тікелей құру әдісі шақырылады . Дегенмен, ағымдағы хэш кестесіндегі шелектердің саны 64-тен аз болса:if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize()
Ағаш құрылымына өтудің орнына элементтерді қайта бөлу арқылы хэш кестесінің өлшемін ұлғайту әдісі шақырылады . өз кезегінде, қызыл-қара ағашқа түрлендірулердіңtreeify()
байланыстырылған тізімі .TreeNode
Әдісresize()
ағымдағы жадтың барлық элементтерін қайталайды, олардың индекстерін қайта есептейді (жаңа өлшемді ескере отырып) және элементтерді жаңа массивке қайта бөледі.Қысқаша айтқанда, қызыл-қара ағаштың құрылымын егжей-тегжейлі қарастырмай, келесідей болады:
-
Тізімнің бірінші элементі бүкіл ағаштың түбірі ретінде жазылады (қара):
if (root == null) { x.parent = null; x.red = false; root = x; }
-
Басқа элементтер үшін:
хэш мәніне қарай оларды солға немесе оңға таратыңыз:
if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1;
Барлық сол жақ балалар түбір түйінінен кіші (немесе оған тең), ал барлық оң жақтағы балалар үлкен болуы керек.
-
Егер екі элементтің хэштері бірдей болса және оларды басқа жолмен салыстыру мүмкін болмаса (олар интерфейсті жүзеге асырмайды
Comparable
), біз ағаштың құрылысын тоқтатып, әдісті шақырамыз , ол өз кезегінде ғаламдық бірегей хэш codeын есептеу үшінtieBreakOrder()
жергілікті әдісті пайдаланады.System.identityHashCode()
.Толығырақ мына жерде: мақалаға сілтеме
-
TreeNode
Ағаш элементтерін (нысандарды ) еншілес (сол немесе оң) нөлдік элемент табылғанша тексереміз .if ((p = (dir <= 0) ? p.left : p.right) == null)
-
Еншілес түйінді қосыңыз (директорға байланысты солға немесе оңға):
x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x;
-
Жаңа элементті қосу ағаштың тепе-теңдігін бұзуы мүмкін болғандықтан, біз қайта теңестіру әдісін шақырамыз:
root = balanceInsertion(root, x);
CCH теңгерімі туралы мына жерден оқи аласыз: habr
-
Теңдестіргеннен кейін түбір элементі өзгеруі мүмкін. Біз мұны оған жіберілген түбір бірінші түйін болатынына кепілдік беретін әдісті шақыру арқылы түзетеміз:
moveRootToFront(tab, root)
Мұнда қызыл-қара ағаштың қалай салынғанын және өзін-өзі теңестіретінін анық көруге болады: визуализация
-
- Кілттің хэш codeын есептеңіз.
- Шелек индексін есептеңіз.
- Индекс арқылы өтіп, бірінші элементтің кілтін бар мәнмен салыстырыңыз. Егер олар тең болса, мәнді қайтарыңыз, әйтпесе ол бар болса, келесі элементті тексеріңіз.
- Келесі Түйін нысаны бос болса, нөлді қайтарыңыз.
- Келесі Түйін нысаны бос емес болса, оған өтіп, элемент табылғанша немесе келесі Түйін нысаны бос болғанша алғашқы үш қадамды қайталаңыз.
get()
біз «KING» кілтінің мәнін аламыз:
map.get("KING");
Ішінде әдіс деп аталады getNode(int hash, Object key)
, оған кілттің өзі («KING») және оның хэші (2306996) жіберіледі, ол операция кезіндегідей алдын ала есептеледі put()
.
-
Біз тексереміз:
-
хэш кестесі тіпті бар ма:
(tab = table) != null
HashMap құру кезінде конструкторда кестеге арналған массив жасалмағанын еске саламын, бұл
resize()
хэш кестесіне бірінші элементті қосқанда әрқашан шақырылатын әдісте кейінірек орын алады. Сондықтан, егер HashMap қолданбасына элементтер қосылмаған болса, элементтерді сақтайтын ішкі жиым жоқ. -
алдыңғы өрнек шын мәнін қайтарса, ішкі массив ұзындығы 0-ден үлкен екеніне көз жеткізу керек:
(n = tab.length) > 0;
-
егер алдыңғы өрнек ақиқат болса, бұрын есептелген индекстегі шелекке (біздің жағдайда 4) өтіп, элементтердің бар-жоғын тексеріңіз:
(first = tab[(n - 1) & hash]) != null)
-
Біз іздеген кілтті шелек ішіндегі тізімдегі бірінші элементтің кілтімен салыстырамыз, өйткені көптеген шелектерде (барлық жерде соқтығыстар болмайды) бір ғана элемент болады (біздің жағдайымыз). Әдіс жағдайындағыдай
put()
, хэштер салыстырылады, ал егер олар сәйкес келсе, кілттер сілтеме бойынша, содан кейін ғана арқылы салыстырыладыequals()
.if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
Біздің жағдайда «KING» пернесі «BLAKE» пернесінен бұрын болатындықтан (байланысты тізімде жаңа элементтер соңына қосылады және алдымен KING қосылды), біз осы жерде тоқтап, бірінші нысанды қайтарамыз. Get() әдісіне Node теріңіз, ол одан (100) мәні бар өрісті «жұлып алады):
return (e = getNode(hash(key), key)) == null ? null : e.value;
-
-
Егер шелек ішінде бірнеше элемент болса, онда:
-
do – while
егер шелек байланыстырылған тізім болса, сәйкестік табылғанша тізімді циклдегі элементтердің әрқайсысы арқылы өткіземіз :do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null);
-
егер шелек ағаш құрылымы болса, онда әдіс қосымша деп аталады
getTreeNode()
, ол өз кезегінде қажетті кілтті табу әдісін пайдаланадыfind()
. Біз ағаш іздеуді жүзеге асырамыз - хэштер салыстырылады және іздеу үшін сол немесе оң түбір түйін анықталады. Егер пернелер тең болса (анықтама бойынша немесе арқылыequals
), осы түйінді қайтарыңыз. Егер сол немесе оң жақ еншілес түйіндер нөл болса, біз салыстыру арқылы пернелерді қосымша салыстырамыз (егер пернелер интерфейсті орындасаComparable
), әйтпесе сәйкестік табылғанша ағаш (оң немесе сол ішкі ағаш) арқылы рекурсивті іздеуді орындаймыз.
-
-
қалаған шелекке барыңыз (қайтадан, ол алдын ала есептелген);
-
шелекте бір ғана нысан болса (біз оның нөлдік көрсеткішін тексереміз) хэштерді, сілтемелерді және
equals
(егер кенеттен хэштер тең болмаса) салыстырамыз. Сәйкес таптыңыз ба? Керемет, бұл біздің кілтіміз - оны жойыңыз (=null) және осы кілттің мәнін қайтарыңыз. -
егер шелекте бірнеше элемент болса, біз элементті тапқанша немесе тізімнің соңына жеткенше циклдегі әрбір элементті тексереміз.
-
егер элемент табылмаса, біз нөлді қайтарамыз.
GO TO FULL VERSION