static int indexFor(int h, int length) {
return h & (length-1);
}
כקלט, הוא לקח את קוד ה-hash שהתקבל כתוצאה מהעבודה hashCode()
ואת אורך המערך הפנימי (מספר התאים). וזה החזיר את התוצאה "קוד hash" -> "AND" -> (אורך מערך - 1). המחלקה HashMap
יורשת מהמחלקה AbstractMap
ומיישמת את הממשקים הבאים: Map
, Cloneable
, Serializable
. ה-.method אחראי על פונקציית ה-hash ב-Java hashCode()
. יישום ברירת המחדל hashCode()
מחזיר ערך שנקרא קוד ה-hash identity . גם אם מחלקה עוקפת את hashCode()
, תמיד תוכל לקבל את ה-hash של ה-ID של האובייקט על ידי קריאה ל- System.identityHashCode(Object o)
. למימוש ברירת המחדל hashCode()
ב-OpenJDK אין שום קשר לכתובת הזיכרון, כפי שחושבים לפעמים. פרטים נוספים כאן: habr ב-HashMap , טבלת הגיבוב מיושמת על בסיס מערך (או ליתר דיוק, דינמי, מכיוון שהטבלה יכולה להתרחב) של רשימות מקושרות בודדות. בעיקרו של דבר, אנו משיגים את קוד ה-hash של המפתח כתוצאה מהשיטה hashCode()
, אשר לאחר מכן משתנה (כפי שנשקול בהמשך), ובפנים, באמצעות שיטה נוספת, הערכים המתקבלים מופצים לתאים הנדרשים. רכיבי מערך (תאים) נקראים גם דליים , המשמשים לאחסון צמתים בודדים. כל דלי הוא אוסף (רשימה או עץ). צומת הוא אובייקט של מחלקה מקוננת Node
(או TreeNode
במבנה עץ). למעשה, בתוך תא המערך שוכנת LinkedList
רק רשימה מקושרת בודדת, או עץ אדום-שחור, העומד בבסיס היישום של מחלקה אחרת - TreeMap
.
HashMap
שבתוכה יש את השדות הבאים:
final int hash
- ה-hash של האלמנט הנוכחי, שאנו מקבלים כתוצאה מה-hash של המפתח;final K key
- המפתח של האלמנט הנוכחי. כאן נכתב מה שאתה מציין כאובייקט הראשון בשיטהput()
;V value
- הערך של האלמנט הנוכחי. ומה שאתה מציין כאובייקט השני בשיטה כתוב כאןput()
;Node < K, V> next
- קישור לצומת הבא בתוך אותו סל. הרשימה מחוברת, אז היא צריכה קישור לא לצומת הבא, אם יש כזה.
transient Node < K, V> [] table
– טבלת הגיבוב עצמה, המיושמת על בסיס מערך, לאחסון צמדי מפתח-ערך בצורה של צמתים. כאן מאוחסנים הצמתים שלנו;transient int size
- מספר זוגות מפתח-ערך;int threshold
- המספר המרבי של אלמנטים, כאשר מגיעים אליו גודל טבלת הגיבוב מוכפל. מחושב באמצעות הנוסחה (קיבולת * loadFactor);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
- הקיבולת המרבית האפשרית של טבלת הגיבוב (כמיליארד);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()
- יוצר תצוגת hash כברירת מחדל: לפי נפח(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()
. בואו נסתכל על השלבים הכרוכים בעת הוספת אובייקט:
-
ערך הגיבוב של המפתח של האובייקט שהוזן מחושב. ה-hash המפתח מחושב על ידי שיטה
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
, которая в дальнейшем используется для вычисления бакета. -
במקביל, אנו מחשבים את אינדקס הדלי בו יונח החפץ שלנו ובודקים האם יש בו כבר אלמנטים. אנו מחשבים:
i = (n - 1) & hash i = (16 - 1) & 2306996 i = 4
חשבון:
if ((p = tab[i = (n - 1) & hash]) == null)
-
מכיוון שכתוצאה מהבדיקה אנו מקבלים אמת (אין אלמנטים ב-bucket), יווצר אובייקט Node עם השדות הבאים:
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
אובייקט ה-Node שנוצר שלנו יתווסף לדלי תחת אינדקס [4]:
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
היא שיטה שפשוט מחזירה אובייקט של המחלקה Node. -
לאחר ההוספה, תיעשה בדיקה כדי לראות אם מספר האלמנטים הנוכחי חורג מהפרמטר
threshold
:if (++size > threshold) resize();
אם חורג, תיקרא שיטה
resize()
להגדלת גודל טבלת הגיבוב.בשלב זה, השיטה
putVal()
(ו, בהתאמהput()
) תשלים את עבודתה.ניתן להציג את התוצאה בצורה גרפית באופן הבא:
באופן כללי, כך נראית הוספת Node (זוג מפתח-ערך) לטבלת hash אם הדלי הרצוי ריק . כעת ננסה להוסיף אלמנט שיוביל להתנגשות (כאשר יש יותר מאלמנט אחד בדלי אחד).
-
- שרשור חיצוני או שיטת שרשור (מיושם ב-HashMap) - כלומר. התא למעשה מכיל רשימה (שרשרת). והרשימה עשויה כבר להכיל מספר ערכים (לא בהכרח עם אותו קוד hash).
- שיטת חיטוט ליניארית או כתובת פתוחה (מיושם ב-IdentityHashMap) - מורכבת מחיפוש התא הריק הראשון אחרי זה שמצביע עליו פונקציית ה-hash;
-
באמצעות השיטה,
put()
נוסיף זוג מפתח-ערך נוסף למיפוי ה-hash:map.put("BLAKE", 10);
-
אנו מחשבים את ה-"hash ראשוני" - 63281361. אנו משנים אותו וכתוצאה מכך נקבל את קוד ה-hash הסופי - 63281940.
-
מכיוון שהבדיקה הראשונה של "ריקות" תחזיר כעת false (אין צורך ליצור טבלה), אנו מיד מחשבים את האינדקס של הדלי שבו האובייקט שלנו ימוקם:
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()
להגדלת גודל טבלת ה-hash, לפיזור מחדש של האלמנטים.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
), אנו קוטעים את בניית העץ וקוראים לשיטהtieBreakOrder()
, אשר בתורה משתמשת בשיטה המקוריתSystem.identityHashCode()
כדי לחשב קוד חשיש ייחודי גלובלי .פרטים נוספים כאן: קישור למאמר
-
אנו בודקים את רכיבי העץ (אובייקטים
TreeNode
) עד שנמצא אלמנט אפס בן (משמאל או ימין).if ((p = (dir <= 0) ? p.left : p.right) == null)
-
הוסף צומת צאצא (משמאל או ימין בהתאם ל-dir):
x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x;
-
מכיוון שהוספת אלמנט חדש עלולה להפר את האיזון של העץ, אנו קוראים לשיטה לאיזון מחדש:
root = balanceInsertion(root, x);
אתה יכול לקרוא על איזון ה-CCH כאן: habr
-
לאחר האיזון, אלמנט השורש עשוי להשתנות. אנו מתקנים זאת על ידי קריאה לשיטה שמבטיחה שהשורש שיועבר אליה יהיה הצומת הראשון:
moveRootToFront(tab, root)
ניתן לראות בבירור כיצד בנוי עץ אדום-שחור ומאזן את עצמו כאן: הדמיה
-
- חשב את קוד הגיבוב של המפתח.
- חשב את מדד הדלי.
- עברו על האינדקס והשוו את המפתח של האלמנט הראשון לערך הקיים. אם הם שווים, החזר את הערך, אחרת בדוק את האלמנט הבא, אם הוא קיים.
- אם אובייקט ה-Node הבא הוא null, החזר null.
- אם אובייקט ה-Node הבא אינו null, עבור אליו וחזור על שלושת השלבים הראשונים עד שהאלמנט נמצא או שאובייקט ה-Node הבא יהיה null.
get()
אנו מקבלים את הערך עבור מפתח "KING":
map.get("KING");
בפנים, השיטה נקראת getNode(int hash, Object key)
, אליה מועברים המפתח עצמו ("KING") וה-hash שלו (2306996), אשר מחושב מראש באותו אופן כמו במהלך הפעולה put()
.
-
אנחנו בודקים:
-
האם בכלל קיימת טבלת גיבוב:
(tab = table) != null
הרשו לי להזכיר לכם שכאשר יוצרים HashMap, מערך לטבלה לא נוצר בקונסטרוקטור; זה קורה בהמשך השיטה
resize()
, שנקראת תמיד כשמוסיפים את האלמנט הראשון לטבלת ה-hash. לכן, אם לא נוספו אלמנטים ל-HashMap, פשוט אין מערך פנימי לאחסון האלמנטים. -
אם הביטוי הקודם מחזיר true, עליך לוודא שאורך המערך הפנימי גדול מ-0:
(n = tab.length) > 0;
-
אם גם הביטוי הקודם מחזיר נכון, עבור אל הדלי במדד (במקרה שלנו 4), שחושב בעבר, ובדקו את נוכחותם של אלמנטים:
(first = tab[(n - 1) & hash]) != null)
-
אנו משווים את המפתח שאנו מחפשים למפתח של האלמנט הראשון ברשימה בתוך הדלי, שכן ברוב הדליים יהיה (לא בכל מקום שיש לנו התנגשויות) רק אלמנט אחד (המקרה שלנו). כמו במקרה של השיטה
put()
, ה-hash מושווה, ואם הם תואמים, המפתחות מושווים לפי הפניה, ורק אז לפיequals()
.if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
מכיוון שבמקרה שלנו, המפתח "KING" יקדים את המפתח "BLAKE" (בתוך רשימה מקושרת, מוסיפים אלמנטים חדשים לסוף, ו-KING נוסף ראשון), נעצור בשלב זה ונחזיר את האובייקט הראשון של הקלידו Node למתודה get() ש"חוטפת" ממנו שדה עם הערך (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
), החזר את הצומת הזה. אם צמתי הילד השמאלי או הימני הם null, אנו משווים בנוסף את המפתחות באמצעות compareTo (אם המפתחות מיישמים את הממשקComparable
), אחרת אנו מבצעים חיפוש רקורסיבי דרך העץ (תת-עץ ימני או שמאלי) עד שנמצא התאמה.
-
-
ללכת לדלי הרצוי (שוב, זה מחושב מראש);
-
אם יש רק אובייקט אחד ב-bucket (אנחנו בודקים את מצביע ה-null שלו) אנחנו משווים גיבוב, קישורים ו
equals
(אם פתאום הגיבובים לא שווים). מצאתם התאמה? מעולה, זה המפתח שלנו - מחק אותו (=null) והחזר את הערך של המפתח הזה. -
אם יש יותר מאלמנט אחד בדלי, נבדוק כל אלמנט בלולאה עד שנמצא את האלמנט או נגיע לסוף הרשימה.
-
אם האלמנט לא נמצא, נחזיר null.
GO TO FULL VERSION