static int indexFor(int h, int length) {
return h & (length-1);
}
Kirish sifatida u ish natijasida olingan xesh kodini hashCode()
va ichki massivning uzunligini (hujayralar soni) oldi. Va natijada "xesh-kod" -> bit bo'yicha "VA" -> (massiv uzunligi - 1) natijani qaytardi. Sinf HashMap
sinfdan meros oladi AbstractMap
va quyidagi interfeyslarni amalga oshiradi: Map
, Cloneable
, Serializable
. .usuli Java'da xesh funksiyasi uchun javobgardir hashCode()
. Standart dastur identifikator xesh kodihashCode()
deb nomlangan qiymatni qaytaradi . Agar sinf bekor qilinsa ham , siz har doim qo'ng'iroq qilish orqali ob'ekt identifikatori xeshini olishingiz mumkin . OpenJDK-dagi standart dastur, ba'zida ishonganidek, xotira manzili bilan hech qanday aloqasi yo'q. Batafsil ma'lumot bu yerda: habr HashMap-da xesh-jadval yakka bog'langan ro'yxatlar massivi (yoki aniqrog'i, dinamik, chunki jadval kengayishi mumkin) asosida amalga oshiriladi. Asosan, biz kalitning xesh kodini usul natijasida olamiz , keyin u o'zgartiriladi (keyinroq ko'rib chiqamiz) va ichki qo'shimcha usul yordamida olingan qiymatlar kerakli hujayralarga taqsimlanadi. Massiv elementlari (hujayralar) ham deyiladi chelaklar , ular alohida tugunlarni saqlash uchun ishlatiladi. Chelaklarning har biri to'plamdir (ro'yxat yoki daraxt). Tugun - ichki o'rnatilgan sinf ob'ekti (yoki daraxt tuzilishida). Darhaqiqat, massiv katakchasi ichida faqat bitta bog'langan ro'yxat yoki boshqa sinfni amalga oshirish asosida joylashgan qizil-qora daraxt yotadi - . hashCode()
System.identityHashCode(Object o)
hashCode()
hashCode()
Node
TreeNode
LinkedList
TreeMap
HashMap
, uning ichida quyidagi maydonlar mavjud:
final int hash
— kalitni xeshlash natijasida olingan joriy elementning xeshi;final K key
— joriy elementning kaliti. Usulda birinchi ob'ekt sifatida ko'rsatgan narsa shu yerda yoziladiput()
;V value
— joriy elementning qiymati. Va usulda ikkinchi ob'ekt sifatida ko'rsatgan narsa shu erda yozilganput()
;Node < K, V> next
— xuddi shu savatdagi keyingi tugunga havola. Ro'yxat ulangan, shuning uchun agar mavjud bo'lsa, keyingi tugunga emas, balki havola kerak.
transient Node < K, V> [] table
– kalit-qiymat juftlarini tugunlar ko'rinishida saqlash uchun massiv asosida amalga oshirilgan xesh-jadvalning o'zi. Bu bizning tugunlarimiz saqlanadigan joy;transient int size
— kalit-qiymat juftliklari soni;int threshold
— elementlarning maksimal soni, unga yetganda xesh-jadvalning o'lchami ikki baravar ortadi. Formuladan foydalanib hisoblangan (imkoniyat * yuklanish faktori);final float loadFactor
— bu parametr joriy xesh-jadval yangi xesh-jadvalni yaratish uchun qanday yuklanish darajasida kerakligini aniqlaydi, ya'ni. xesh-jadval 75% to'lishi bilanoq, yangi xesh-jadval yaratiladi va joriy elementlar unga ko'chiriladi (xarajat operatsiya, chunki barcha elementlarni qayta tiklash kerak);transient Set< Map.Entry< K,V>> entrySet
- keshlanganni o'z ichiga oladientrySet()
, biz uni takrorlashimiz mumkinHashMap
.
static final int DEFAULT_INITIAL_CAPACITY= 1 << 4
— standart xesh-jadval sig'imi (16);static final int MAXIMUM_CAPACITY = 1 << 30
— xesh-jadvalning mumkin bo'lgan maksimal sig'imi (taxminan 1 milliard);static final float DEFAULT_LOAD_FACTOR = 0.75f
— standart yuk koeffitsienti;static final int TREEIFY_THRESHOLD = 8
- bu bitta chelakdagi elementlar sonining "eshigi" bo'lib, unga erishilganda ichki bog'langan ro'yxat daraxt tuzilishiga (qizil-qora daraxt) aylanadi.static final int UNTREEIFY_THRESHOLD = 6
— agar bitta savatdagi elementlar soni 6 tagacha kamaysa, u holda daraxtdan bog'langan ro'yxatga teskari o'tish sodir bo'ladi;static final int MIN_TREEIFY_CAPACITY = 64
- daraxt tuzilishiga o'tish mumkin bo'lgan hash-jadvalning minimal sig'imi (chelaklar soni). Bular. Agar xesh jadvalida kamida 64 chelak bo'lsa va bitta chelakda 8 yoki undan ortiq element mavjud bo'lsa, unda daraxt tuzilishiga o'tish sodir bo'ladi.
public HashMap()
— sukut bo'yicha xesh-displeyni yaratadi: hajm bo'yicha(capacity) = 16
va yuk koeffitsienti bilan(load factor) = 0.75
;public HashMap(Map< ? extends K, ? extends V> m)
— boshqa xaritalash elementlarini joylashtirish uchun yetarli bo'lgan boshlang'ich sig'imga ega bo'lgan boshqa berilgan xaritalash elementlari tomonidan inisializatsiyalangan xesh xaritalashni yaratadi;public HashMap(int initialCapacity)
— berilgan boshlang‘ich sig‘imga ega xesh-xaritani yaratadi. HashMap to'g'ri va to'g'ri ishlashi uchun ichki massivning o'lchami ikkita (ya'ni 16, 64, 128 va boshqalar) bo'lishi kerak;public HashMap(int initialCapacity, float loadFactor)
— belgilangan parametrlarga ega xesh xaritasini yaratadi: dastlabki quvvat va yuk koeffitsienti.
Map
va sinfni AbstractMap
o'z usullarini qo'shmasdan kengaytiradi. Xesh xaritasi uning elementlari tartibini kafolatlamaydi. Shuning uchun, elementlarning xesh xaritasiga kiritilish tartibi ularni iterator tomonidan olish tartibi bo'lishi shart emas. Ob'ektlarni qo'shish Kalit-qiymat juftligini qo'shish yordamida amalga oshiriladi put()
. Ob'ektni qo'shish bilan bog'liq qadamlarni ko'rib chiqaylik:
-
Kiritilgan ob'ekt kalitining xesh qiymati hisoblanadi. Kalit xesh biz bilgan kalit usuliga
static final int hash(Object key)
allaqachon kirish usuli bilan hisoblanadi .hashCode()
Buning uchun bekor qilingan usulhashCode()
yoki uning sukut bo'yicha amalga oshirilishi qo'llaniladi. Usuldagi qo'shimcha o'zgarishlarhash()
: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
, которая в дальнейшем используется для вычисления бакета. -
Shu bilan birga, biz ob'ektimiz joylashtiriladigan chelak indeksini hisoblaymiz va unda allaqachon elementlar mavjudligini tekshiramiz. Biz hisoblaymiz:
i = (n - 1) & hash i = (16 - 1) & 2306996 i = 4
tekshiring:
if ((p = tab[i = (n - 1) & hash]) == null)
-
Tekshirish natijasida biz haqiqatga erishamiz (paqirda hech qanday element yo'q), quyidagi maydonlarga ega Node ob'ekti yaratiladi:
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
Bizning yaratilgan Node ob'ektimiz indeks ostidagi paqirga qo'shiladi [4]:
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
oddiygina Node sinfining ob'ektini qaytaradigan usuldir. -
Qo'shgandan so'ng, elementlarning joriy soni parametrdan oshib ketganligini tekshirish amalga oshiriladi
threshold
:if (++size > threshold) resize();
resize()
Agar oshib ketgan bo'lsa, xesh jadvalining hajmini oshirish uchun usul chaqiriladi .Bu vaqtda usul
putVal()
(va mos ravishdaput()
) o'z ishini yakunlaydi.Natijani grafik tarzda quyidagicha ifodalash mumkin:
Umuman olganda, agar kerakli chelak bo'sh bo'lsa, xesh jadvaliga tugunni (kalit-qiymat juftligini) qo'shish shunday ko'rinadi . Keling, to'qnashuvga olib keladigan elementni qo'shishga harakat qilaylik (bir chelakda bir nechta element mavjud bo'lganda).
-
- tashqi zanjir yoki zanjir usuli (HashMapda amalga oshiriladi) - ya'ni. hujayra aslida ro'yxatni (zanjirni) o'z ichiga oladi. Va ro'yxat allaqachon bir nechta qiymatlarni o'z ichiga olishi mumkin (bir xil xesh-kod bilan emas).
- chiziqli tekshirish yoki ochiq manzillash usuli (IdentityHashMap-da amalga oshirilgan) - xesh funktsiyasi ko'rsatganidan keyin birinchi bo'sh katakchani qidirishdan iborat;
-
Usuldan foydalanib,
put()
biz xesh xaritalash uchun yana bir kalit-qiymat juftligini qo'shamiz:map.put("BLAKE", 10);
-
Biz "dastlabki hash" ni hisoblaymiz - 63281361. Biz uni o'zgartiramiz va natijada biz yakuniy hash kodini olamiz - 63281940.
-
Birinchi "bo'shlik" tekshiruvi endi noto'g'ri bo'lganligi sababli (jadval yaratishning hojati yo'q), biz darhol ob'ektimiz joylashtiriladigan chelak indeksini hisoblaymiz:
i = (n - 1) & hash i = (16 - 1) & 63281940 i = 4
-
Belgilangan indeksdagi chelak undagi elementlarning mavjudligi uchun tekshiriladi va
if ((p = tab[i = (n - 1) & hash]) == null)
bu holda shart bajarilmaganligi sababli (paqirda allaqachon element mavjud), keyin biz blokga o'tamizelse
. -
Avvalo, biz qo'shilgan elementni chelak ichidagi bog'langan ro'yxatning birinchi elementi bilan taqqoslaymiz:
(p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
При проверке сначала сравниваются хеши ключей. Если этот «участок»
(p.hash == hash)
возвращает false, тогда остальная часть условия игнорируется (&&
), так How an objectы гарантированно разные. Иначе затем сравниваются ключи по ссылке (==) и в случае неequalsства, ключи сравниваются уже посредством методаequals()
. Сравнение осуществляется в таком порядке во благо производительности. Если все три выражения возвращают true, это означает, что ключи равны и новый элемент будет записан в дополнительную переменную, чтобы в дальнейшем с её помощью перезаписать meaning у ключа:if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
В результате сравнения ключей мы получаем false уже на первом этапе (разные хеши).
-
Игнорируем condition (
p instanceof TreeNode
), так How структура в бакете не является древовидной на данном этапе. -
Далее переходим в цикл
for
, где в пределах одного бакета проверяем у элементов указатель на следующий элементnext
, и если он equals null (значит элемент в списке последний и единственный), добавляем новый элемент Node в конец списка:if ((e = p.next) == null){ p.next = newNode(hash, key, value, null) ... };
Вы можете спросить, а где же проверка на equalsство ключей? Мы же не можем просто взять и добавить новый элемент. Так вот она уже была заранее осуществлена в пункте (5). Благодаря этому, теперь мы можем просто проверить указатель этого element, и так How он сейчас equals null, можно сделать вывод о том, что в списке только один элемент. И так How мы уже проверяли их ключи, мы можем безболезненно добавлять новый элемент.
Если же при первой итерации указатель не equals null, это говорит о том, что в списке How минимум два element, поэтому в таком случае мы переходим к следующему условия и сравниваем ключ добавляемого element со всеми ключами элементов в списке (способом, описанным в пятом пункте).
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
Если при сравнении ключей будет найдено совпадение, новый элемент будет записан в дополнительную переменную, чтобы в дальнейшем с её помощью перезаписать meaning у ключа.
После добавления второго element наш an object HashMap графически выглядит так:
В итоге, добавление элементов при коллизии (в пределах одного бакета) выглядит следующим образом:
- проверяем с помощью методов
hashCode()
иequals()
, что оба ключа одинаковы. - если ключи одинаковы, заменить текущее meaning новым.
- иначе связать новый и старый an objectы с помощью структуры данных "связный список", указав ссылку на следующий an object в текущем и сохранить оба под нужным индексом; либо осуществить переход к древовидной структуре
- проверяем с помощью методов
-
После каждой итерации (добавления нового element) в цикле for увеличивается счетчик, который отвечает за количество элементов в бакете:
for (int binCount = 0; ; ++binCount)
До тех пор, пока их количество не станет равно or больше 7:
binCount >= TREEIFY_THRESHOLD – 1
В таком случае произойдет вызов метода
treeifyBin()treeify()
для непосредственного построения древовидной структуры. Однако, если количество бакетов в текущей хеш-таблице меньше 64:if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize()
Daraxt tuzilishiga o'tish o'rniga, elementlarni qayta taqsimlash, xesh jadvalining hajmini oshirish usuli chaqiriladi . o'z navbatida, qizil-qora daraxtga aylanadigantreeify()
bog'langan ro'yxat .TreeNode
Usulresize()
joriy xotiraning barcha elementlarini takrorlaydi, ularning indekslarini qayta hisoblaydi (yangi o'lchamni hisobga olgan holda) va elementlarni yangi massivga qayta taqsimlaydi.Qisqacha aytganda, qizil-qora daraxtning tuzilishi haqida batafsil ma'lumot bermasdan, quyidagilar sodir bo'ladi:
-
Ro'yxatning birinchi elementi butun daraxtning ildizi sifatida yoziladi (qora):
if (root == null) { x.parent = null; x.red = false; root = x; }
-
Boshqa elementlar uchun:
hash qiymatiga qarab ularni chapga yoki o'ngga taqsimlang:
if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1;
Barcha chap bolalar ildiz tugunidan kichik (yoki teng) bo'lishi kerak, barcha o'ng bolalar esa kattaroq bo'lishi kerak.
-
Agar ikkita element bir xil xeshlarga ega bo'lsa va ularni boshqa yo'l bilan taqqoslab bo'lmasa (ular interfeysni amalga oshirmasa
Comparable
), biz daraxtning qurilishini to'xtatamiz va usulni chaqiramiz , bu esa o'z navbatida global noyob xesh kodini hisoblash uchuntieBreakOrder()
mahalliy usuldan foydalanadi.System.identityHashCode()
.Batafsil ma'lumot bu yerda: maqolaga havola
-
Biz daraxt elementlarini (ob'ektlar
TreeNode
) bola (chap yoki o'ng) nol elementi topilmaguncha tekshiramiz.if ((p = (dir <= 0) ? p.left : p.right) == null)
-
Bola tugunini qo'shing (direktga qarab chap yoki o'ng):
x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x;
-
Yangi elementni qo'shish daraxtning muvozanatini buzishi mumkinligi sababli, biz muvozanatni tiklash usulini chaqiramiz:
root = balanceInsertion(root, x);
CCHni muvozanatlash haqida bu erda o'qishingiz mumkin: habr
-
Balanslashtirgandan so'ng, ildiz elementi o'zgarishi mumkin. Buni, unga o'tgan ildiz birinchi tugun bo'lishini kafolatlaydigan usulni chaqirish orqali tuzatamiz:
moveRootToFront(tab, root)
Siz bu erda qizil-qora daraxt qanday qurilganini va o'z-o'zini muvozanatlashini aniq ko'rishingiz mumkin: vizualizatsiya
-
- Kalitning xesh kodini hisoblang.
- Paqir indeksini hisoblang.
- Indeksdan o'ting va birinchi elementning kalitini mavjud qiymat bilan solishtiring. Agar ular teng bo'lsa, qiymatni qaytaring, aks holda keyingi element mavjudligini tekshiring.
- Agar keyingi tugun ob'ekti null bo'lsa, nullni qaytaring.
- Agar keyingi Node ob'ekti null bo'lmasa, unga o'ting va element topilguncha yoki keyingi Node ob'ekti null bo'lgunga qadar dastlabki uchta qadamni takrorlang.
get()
biz "KING" kalitining qiymatini olamiz:
map.get("KING");
Ichkarida usul chaqiriladi getNode(int hash, Object key)
, unga kalitning o'zi ("KING") va uning xeshi (2306996) uzatiladi, bu operatsiya paytida bo'lgani kabi oldindan hisoblab chiqiladi put()
.
-
Biz tekshiramiz:
-
hatto hash jadvali mavjudmi:
(tab = table) != null
Sizga eslatib o'tamanki, HashMap yaratishda jadval uchun massiv konstruktorda yaratilmaydi; bu keyinchalik
resize()
xesh jadvaliga birinchi element qo'shilganda chaqiriladigan usulda keyinroq sodir bo'ladi. Shuning uchun, agar HashMap-ga hech qanday element qo'shilmagan bo'lsa, elementlarni saqlash uchun oddiygina ichki massiv yo'q. -
agar oldingi ifoda rost bo'lsa, ichki massiv uzunligi 0 dan katta ekanligiga ishonch hosil qilishingiz kerak:
(n = tab.length) > 0;
-
agar oldingi ifoda ham rost bo'lsa, avval hisoblangan indeksdagi chelakka (bizning holatimizda 4) o'ting va elementlarning mavjudligini tekshiring:
(first = tab[(n - 1) & hash]) != null)
-
Biz izlayotgan kalitni chelak ichidagi ro'yxatdagi birinchi elementning kaliti bilan taqqoslaymiz, chunki ko'pchilik chelaklarda (hamma joyda bizda to'qnashuvlar bo'lmaydi) faqat bitta element (bizning ishimiz) bo'ladi. Usul holatida bo'lgani kabi
put()
, xeshlar solishtiriladi va agar ular mos kelsa, kalitlar mos yozuvlar bo'yicha va faqat keyin tomonidan taqqoslanadiequals()
.if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
Bizning holatimizda "KING" kaliti "BLAKE" kalitidan oldin bo'lgani uchun (bog'langan ro'yxatda yangi elementlar oxiriga qo'shiladi va birinchi navbatda KING qo'shiladi), biz shu nuqtada to'xtab, birinchi ob'ektni qaytaramiz. Get() usuliga Node ni kiriting, u undan (100) qiymatiga ega bo'lgan maydonni "tortib oladi":
return (e = getNode(hash(key), key)) == null ? null : e.value;
-
-
Agar chelak ichida bir nechta element bo'lsa, unda:
-
do – while
agar chelak bog'langan ro'yxat bo'lsa, moslik topilmaguncha biz ro'yxatni tsikldagi elementlarning har biri orqali ko'rib chiqamiz :do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null);
-
agar chelak daraxt tuzilishi bo'lsa, u holda usul qo'shimcha ravishda chaqiriladi
getTreeNode()
, bu esa o'z navbatida kerakli kalitni topish uchun usuldan foydalanadifind()
. Biz daraxt qidirishni amalga oshiramiz - xeshlar solishtiriladi va qidirish uchun chap yoki o'ng ildiz tugunlari aniqlanadi. Agar tugmalar teng bo'lsa (mos yozuvlar bo'yicha yoki tomonidanequals
), bu tugunni qaytaring. Agar chap yoki o'ng pastki tugunlar null bo'lsa, biz qo'shimcha ravishda compareTo yordamida kalitlarni solishtiramiz (agar kalitlar interfeysni amalga oshirsaComparable
), aks holda daraxt bo'ylab (o'ng yoki chap pastki daraxt) mos keladigan topilmaguncha rekursiv qidiruvni amalga oshiramiz.
-
-
kerakli chelakka o'ting (yana, u oldindan hisoblab chiqilgan);
-
chelakda faqat bitta ob'ekt bo'lsa (biz uning null ko'rsatgichini tekshiramiz) biz xeshlarni, havolalarni va
equals
(agar to'satdan xeshlar teng bo'lmasa) solishtiramiz. Gugurt topdingizmi? Ajoyib, bu bizning kalitimiz - uni o'chiring (=null) va ushbu kalitning qiymatini qaytaring. -
agar chelakda bir nechta element mavjud bo'lsa, biz elementni topgunimizcha yoki ro'yxatning oxiriga yetguncha har bir elementni tsiklda tekshiramiz.
-
agar element topilmasa, biz nullni qaytaramiz.
GO TO FULL VERSION