static int indexFor(int h, int length) {
return h & (length-1);
}
Киргизүү катары ал иштин натыйжасында алынган хэш-codeду hashCode()
жана ички массивдин узундугун (уячалардын саны) алды. Ал "хэш code" -> биттик "ЖАНА" -> (массив узундугу – 1) натыйжасын кайтарды. Класс HashMap
класстан мурас алат AbstractMap
жана төмөнкү интерфейстерди ишке ашырат: Map
, Cloneable
, Serializable
. .метод Javaдагы хэш функциясы үчүн жооп берет hashCode()
. Демейки ишке ашыруу иденттүүлүк хэш codeуhashCode()
деп аталган маанини кайтарат . Класс жокко чыгарылса дагы , an objectтин ID хэшти ар дайым чалуу аркылуу ала аласыз . OpenJDKдеги демейки ишке ашыруунун эстутум дареги менен эч кандай байланышы жок, кээде ойлогондой. Көбүрөөк маалымат бул жерде: habr HashMapта хэш tableсы жалгыз байланышкан тизмелердин массивинин (же тагыраак айтканда динамикалык, анткени table кеңейиши мүмкүн) негизинде ишке ашырылат. Негизи, биз ыкманын натыйжасында ачкычтын хэш codeун алабыз , ал кийинчерээк өзгөртүлөт (кийин карап чыгабыз) жана ички кошумча ыкманы колдонуу менен, алынган маанилер керектүү уячаларга бөлүштүрүлөт. Массив элементтери (уячалар) жеке түйүндөрдү сактоо үчүн колдонулган чака деп да аталат . Чакалардын ар бири коллекция (тизме же дарак). Түйүн - уя салынган класстын an objectиси (же дарак структурасында). Чынында, массив уячасынын ичинде башка классты ишке ашыруунун негизин түзгөн жалгыз байланышкан тизме же кызыл-кара дарак жатат - . hashCode()
System.identityHashCode(Object o)
hashCode()
hashCode()
Node
TreeNode
LinkedList
TreeMap
HashMap
анын ичинде төмөнкү талаалар бар:
final int hash
— ачкычты хэширлөөнүн натыйжасында биз алган учурдагы элементтин хэши;final K key
— учурдагы элементтин ачкычы. Бул жерде сиз методдо биринчи an object катары көрсөткөн нерсе жазылатput()
;V value
— учурдагы элементтин мааниси. Ал эми методдо экинчи an object катары көрсөткөн нерсе бул жерде жазылганput()
;Node < K, V> next
— ошол эле себеттин ичиндеги кийинки түйүнгө шилтеме. Тизме туташкан, андыктан ал бар болсо, кийинки түйүнгө эмес, шилтеме керек.
transient Node < K, V> [] table
– түйүндөр түрүндө ачкыч-маани түгөйлөрүн сактоо үчүн массивдин негизинде ишке ашырылган хэш tableнын өзү. Бул жерде биздин түйүндөр сакталат;transient int size
— ачкыч-маанorк жуптардын саны;int threshold
— элементтердин максималдуу саны, ага жеткенде хэш tableнын өлчөмү эки эсеге көбөйөт. Формула менен эсептелген (кубаттуулук * жүктөө фактору);final float loadFactor
— бул параметр жаңы хэш tableсын түзүү үчүн учурдагы хэш-tableга жүктөөнүн кандай даражасында керек экендигин аныктайт, б.а. хэш-table 75% толгондо, жаңы хэш-table түзүлөт жана ага учурдагы элементтер жылдырылат (кымбат операция, анткени бардык элементтерди кайра өзгөртүү керек);transient Set< Map.Entry< K,V>> entrySet
entrySet()
- биз кайталай турган кэшти камтыйтHashMap
.
static final int DEFAULT_INITIAL_CAPACITY= 1 << 4
— демейки хэш tableнын сыйымдуулугу (16);static final int MAXIMUM_CAPACITY = 1 << 30
— хэш tableнын максималдуу мүмкүн болгон сыйымдуулугу (болжол менен 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
— дарак структурасына өтүү мүмкүн болгон хэш tableнын минималдуу сыйымдуулугу (чакалардын саны). Ошол. Эгерде хэш tableсында жок дегенде 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()
-
Киргизилген an objectтин ачкычынын хэш мааниси эсептелет. Ачкыч хэш биз билген ачкыч ыкмасына
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)
-
Текшерүүнүн натыйжасында биз чындыкка жеткендиктен (чакада эч кандай элементтер жок), төмөнкү талаалары бар Node an objectи түзүлөт:
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
Биздин түзүлгөн Node an objectиси [4] индексинин астындагы чакага кошулат:
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
жөн гана Node классынын an objectисин кайтарган ыкма. -
Кошулгандан кийин, элементтердин учурдагы саны параметрден ашып кеткендигин текшерүү үчүн текшерүү жүргүзүлөт
threshold
:if (++size > threshold) resize();
resize()
ашып кетсе, анда хэш tableнын өлчөмүн көбөйтүү үчүн ыкма чакырылат .Бул учурда, ыкма
putVal()
(жана, тиешелүүлүгүнө жарашаput()
) өз ишин аяктайт.Жыйынтыгын графикалык түрдө төмөнкүчө чагылдырууга болот:
Жалпысынан алганда, хэш tableга Түйүндү (ачкыч-маани жуптарын) кошуу, эгер каалаган чака бош болсо, ушундай көрүнөт . Эми кагылышууга алып келе турган элементти кошууга аракет кылалы (бир чакада бирден ашык элемент болгондо).
-
- тышкы чынжыр же чынжыр ыкмасы (HashMap ишке ашырылган) - б.а. клетка чындыгында тизмени (чынжыр) камтыйт. Ал эми тизме буга чейин бир нече маанилерди камтышы мүмкүн (сөзсүз эле хэш-code менен эмес).
- сызыктуу текшерүү же ачык даректөө ыкмасы (IdentityHashMap программасында ишке ашырылган) - хэш-функция көрсөткөндөн кийин биринчи бош уячаны издөөдөн турат;
-
Методду колдонуп,
put()
хэш картасына дагы бир ачкыч-маани жуптарын кошобуз:map.put("BLAKE", 10);
-
Биз "алдын ала хэшти" эсептейбиз - 63281361. Биз аны өзгөртүп, натыйжада акыркы хэш codeун алабыз - 63281940.
-
"Боштуктун" биринчи текшерүүсү эми жалганды кайтара тургандыктан (table түзүүнүн кереги жок), биз дароо биздин 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)
возвращает 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()
Дарак структурасына баруунун ордуна, элементтерди кайра бөлүштүрүү, хэш tableнын өлчөмүн көбөйтүү ыкмасы чакырылат . өз кезегинде, кызыл-кара даракка айландырган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()
жергorктүү ыкманы колдонот.System.identityHashCode()
.Көбүрөөк маалымат бул жерде: макалага шилтеме
-
Биз дарак элементтерин (an objectтерди
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ун эсептеңиз.
- Чака индексин эсептөө.
- Индекс аркылуу өтүп, биринчи элементтин ачкычын учурдагы маани менен салыштырыңыз. Эгерде алар бирдей болсо, маанини кайтарыңыз, антпесе кийинки элементтин бар-жогун текшериңиз.
- Эгерде кийинки Түйүн an objectиси нөл болсо, нөлдү кайтарыңыз.
- Эгерде кийинки Түйүн an objectиси нөл эмес болсо, ага өтүп, элемент табылганга чейин же кийинки Түйүн an objectиси нөл болгонго чейин биринчи үч кадамды кайталаңыз.
get()
биз "KING" ачкычынын маанисин алабыз:
map.get("KING");
Ичинде, ыкма деп аталат getNode(int hash, Object key)
, ага ачкычтын өзү ("KING") жана анын хэштери (2306996) өткөрүлөт, ал операция учурундагыдай эле алдын ала эсептелген put()
.
-
Биз текшеребиз:
-
хэш tableсы да барбы:
(tab = table) != null
Эске сала кетейин, HashMap түзүүдө table үчүн массив конструктордо түзүлбөйт, бул
resize()
хэш tableга биринчи элементти кошкондо дайыма чакырылган методдо кийинчерээк болот. Демек, 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 биринчи кошулду), биз ушул жерден токтоп, биринчи an objectти кайтарабыз. 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
) бул түйүндү кайтарыңыз. Эгерде сол же оң бала түйүндөр нөл болсо, биз кошумча баскычтарды compareTo аркылуу салыштырабыз (эгерде баскычтар интерфейсти ишке ашырсаComparable
), антпесе дал келгенге чейин дарак (оң же сол суб дарак) аркылуу рекурсивдүү издөө жүргүзөбүз.
-
-
каалаган чака барып (кайра, ал алдын ала эсептелген);
-
чакада бир гана an object болсо (биз анын нөл көрсөткүчүн текшеребиз) хэштерди, шилтемелерди жана
equals
(эгерде күтүлбөгөн жерден хэштер бирдей болбосо) салыштырабыз. Дал келдиңизби? Жакшы, бул биздин ачкыч - аны жок кылуу (=нөл) жана бул ачкычтын маанисин кайтарыңыз. -
эгерде чакада бирден ашык элемент болсо, биз элементти тапканга же тизменин аягына жеткенге чейин циклдин ар бир элементин текшеребиз.
-
эгерде элемент табылбаса, биз нөлдү кайтарабыз.
GO TO FULL VERSION