static int indexFor(int h, int length) {
return h & (length-1);
}
Làm đầu vào, nó lấy mã băm thu được do công việc hashCode()
và độ dài của mảng bên trong (số lượng ô). Và nó trả về kết quả “mã băm” -> bitwise “AND” -> (độ dài mảng – 1). Lớp HashMap
kế thừa từ lớp AbstractMap
và thực hiện các giao diện sau: Map
, Cloneable
, Serializable
. Phương thức .method chịu trách nhiệm về hàm băm trong Java hashCode()
. Việc triển khai mặc định hashCode()
trả về một giá trị được gọi là mã băm nhận dạng . Ngay cả khi một lớp ghi đè hashCode()
, bạn luôn có thể lấy mã băm ID của đối tượng bằng cách gọi System.identityHashCode(Object o)
. Việc triển khai mặc định hashCode()
trong OpenJDK không liên quan gì đến địa chỉ bộ nhớ, như đôi khi người ta vẫn tin. Xem thêm chi tiết tại đây: habr Trong HashMap , bảng băm được triển khai dựa trên một mảng (hay chính xác hơn là động, vì bảng có thể mở rộng) của các danh sách liên kết đơn lẻ. Về cơ bản, chúng tôi nhận được mã băm của khóa nhờ phương thức hashCode()
, sau đó được sửa đổi (như chúng tôi sẽ xem xét sau) và nội bộ, bằng cách sử dụng một phương thức bổ sung, các giá trị kết quả sẽ được phân phối đến các ô cần thiết. Các phần tử mảng (ô) còn được gọi là nhóm , được sử dụng để lưu trữ các nút riêng lẻ. Mỗi nhóm là một bộ sưu tập (danh sách hoặc cây). Nút là một đối tượng của một lớp lồng nhau Node
(hoặc TreeNode
trong cấu trúc cây). Trên thực tế, bên trong ô mảng LinkedList
chỉ có một danh sách liên kết đơn hoặc một cây màu đỏ đen, làm cơ sở cho việc triển khai một lớp khác - TreeMap
.
HashMap
có các trường sau:
final int hash
— hàm băm của phần tử hiện tại mà chúng ta thu được nhờ băm khóa;final K key
- khóa của phần tử hiện tại. Đây là nơi những gì bạn chỉ định làm đối tượng đầu tiên trong phương thức được ghi vàoput()
;V value
- giá trị của phần tử hiện tại. Và những gì bạn chỉ định làm đối tượng thứ hai trong phương thức được viết ở đâyput()
;Node < K, V> next
- liên kết tới nút tiếp theo trong cùng một giỏ. Danh sách được kết nối, vì vậy nó cần một liên kết không đến nút tiếp theo, nếu có.
transient Node < K, V> [] table
– bản thân bảng băm, được triển khai trên cơ sở một mảng, để lưu trữ các cặp khóa-giá trị dưới dạng nút. Đây là nơi lưu trữ các Nút của chúng tôi;transient int size
- số cặp khóa-giá trị;int threshold
— số phần tử tối đa, khi đạt đến kích thước của bảng băm sẽ tăng gấp đôi. Tính theo công thức (công suất * LoadFactor);final float loadFactor
— tham số này xác định mức độ tải mà bảng băm hiện tại cần để tạo bảng băm mới, tức là ngay khi bảng băm đầy 75%, một bảng băm mới sẽ được tạo và các phần tử hiện tại sẽ được chuyển vào đó (một hoạt động tốn kém, vì tất cả các phần tử phải được băm lại);transient Set< Map.Entry< K,V>> entrySet
- chứa một bộ nhớ đệmentrySet()
mà chúng ta có thể lặp lạiHashMap
.
static final int DEFAULT_INITIAL_CAPACITY= 1 << 4
— dung lượng bảng băm mặc định (16);static final int MAXIMUM_CAPACITY = 1 << 30
- dung lượng tối đa có thể có của bảng băm (khoảng 1 tỷ);static final float DEFAULT_LOAD_FACTOR = 0.75f
- hệ số tải mặc định;static final int TREEIFY_THRESHOLD = 8
là “ngưỡng” số phần tử trong một nhóm, khi đạt đến ngưỡng đó danh sách liên kết nội bộ sẽ được chuyển thành cấu trúc cây (cây đỏ đen).static final int UNTREEIFY_THRESHOLD = 6
— nếu số phần tử trong một giỏ giảm xuống còn 6 thì sẽ xảy ra quá trình chuyển đổi ngược từ cây sang danh sách liên kết;static final int MIN_TREEIFY_CAPACITY = 64
— dung lượng tối thiểu (số lượng nhóm) của bảng băm mà tại đó có thể chuyển đổi sang cấu trúc cây. Những thứ kia. Nếu có ít nhất 64 nhóm trong bảng băm và có 8 phần tử trở lên trong một nhóm thì quá trình chuyển đổi sang cấu trúc cây sẽ xảy ra.
public HashMap()
— tạo màn hình băm theo mặc định: theo âm lượng(capacity) = 16
và theo hệ số tải(load factor) = 0.75
;public HashMap(Map< ? extends K, ? extends V> m)
— tạo một ánh xạ băm được khởi tạo bởi các phần tử của một ánh xạ nhất định khác với dung lượng ban đầu đủ để chứa các phần tử của ánh xạ khác;public HashMap(int initialCapacity)
— tạo bản đồ băm với dung lượng ban đầu nhất định. Để HashMap hoạt động chính xác và chính xác, kích thước của mảng bên trong phải là lũy thừa của hai (tức là 16, 64, 128, v.v.);public HashMap(int initialCapacity, float loadFactor)
— tạo bản đồ băm với các tham số được chỉ định: công suất ban đầu và hệ số tải.
Map
và mở rộng một lớp AbstractMap
mà không cần thêm các phương thức riêng của nó. Bản đồ băm không đảm bảo thứ tự các phần tử của nó. Do đó, thứ tự các phần tử được nhập vào bản đồ băm không nhất thiết là thứ tự mà trình vòng lặp truy xuất chúng. Thêm đối tượng Việc thêm cặp khóa-giá trị được thực hiện bằng cách sử dụng put()
. Hãy xem các bước liên quan khi thêm một đối tượng:
-
Giá trị băm của khóa của đối tượng đã nhập được tính toán. Hàm băm khóa được tính bằng một phương thức
static final int hash(Object key)
đã truy cập vàohashCode()
phương thức khóa mà chúng ta biết. Để thực hiện việc này, phương thức được ghi đèhashCode()
hoặc cách triển khai mặc định của nó sẽ được sử dụng. Các phép biến đổi bổ sung trong phương thứchash()
: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
, которая в дальнейшем используется для вычисления бакета. -
Đồng thời, chúng tôi tính toán chỉ mục của nhóm nơi đối tượng của chúng tôi sẽ được đặt và kiểm tra xem đã có phần tử nào trong đó chưa. Chúng tôi tính toán:
i = (n - 1) & hash i = (16 - 1) & 2306996 i = 4
kiểm tra:
if ((p = tab[i = (n - 1) & hash]) == null)
-
Vì kết quả kiểm tra cho thấy chúng tôi nhận được kết quả đúng (không có phần tử nào trong nhóm), nên một đối tượng Node có các trường sau sẽ được tạo:
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
Đối tượng Node được tạo của chúng tôi sẽ được thêm vào nhóm theo chỉ mục [4]:
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
là một phương thức chỉ đơn giản trả về một đối tượng của lớp Node. -
Sau khi thêm, sẽ tiến hành kiểm tra xem số phần tử hiện tại có vượt quá tham số hay không
threshold
:if (++size > threshold) resize();
Nếu vượt quá thì một phương thức sẽ được gọi
resize()
để tăng kích thước của bảng băm.Tại thời điểm này, phương thức
putVal()
(và , tương ứngput()
) sẽ hoàn thành công việc của nó.Kết quả có thể được biểu diễn bằng đồ họa như sau:
Nói chung, đây là giao diện khi thêm Nút (cặp khóa-giá trị) vào bảng băm nếu nhóm mong muốn trống . Bây giờ, hãy thử thêm một phần tử sẽ dẫn đến xung đột (khi có nhiều hơn một phần tử trong một nhóm).
-
- phương pháp xâu chuỗi hoặc xâu chuỗi bên ngoài (được triển khai trong HashMap) - tức là ô thực sự chứa một danh sách (chuỗi). Và danh sách có thể đã chứa một số giá trị (không nhất thiết phải có cùng mã băm).
- phương pháp thăm dò tuyến tính hoặc địa chỉ mở (được triển khai trong IdentityHashMap) - bao gồm tìm kiếm ô trống đầu tiên sau ô được hàm băm trỏ đến;
-
Sử dụng phương thức này,
put()
chúng tôi sẽ thêm một cặp khóa-giá trị khác vào ánh xạ băm:map.put("BLAKE", 10);
-
Chúng tôi tính toán “băm sơ bộ” – 63281361. Chúng tôi sửa đổi nó và kết quả là chúng tôi nhận được mã băm cuối cùng – 63281940.
-
Vì lần kiểm tra đầu tiên về "sự trống rỗng" bây giờ sẽ trả về sai (không cần tạo bảng), nên chúng tôi tính toán ngay chỉ mục của nhóm nơi đối tượng của chúng tôi sẽ được đặt:
i = (n - 1) & hash i = (16 - 1) & 63281940 i = 4
-
Nhóm tại chỉ mục đã chỉ định sẽ được kiểm tra sự hiện diện của các phần tử trong đó và vì điều kiện
if ((p = tab[i = (n - 1) & hash]) == null)
không được đáp ứng trong trường hợp này (đã có một phần tử trong nhóm), nên chúng ta chuyển đến khốielse
. -
Trước hết, chúng tôi so sánh phần tử được thêm với phần tử đầu tiên của danh sách được liên kết bên trong nhóm:
(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)
Thay vì đi vào cấu trúc cây, một phương thức sẽ được gọi
resize()
để tăng kích thước của bảng băm, phân phối lại các phần tử.treeify()
lần lượt, danh sách liên kếtTreeNode
chuyển đổi thành cây đỏ đen. Phương thứcresize()
lặp qua tất cả các phần tử của bộ lưu trữ hiện tại, tính toán lại chỉ số của chúng (có tính đến kích thước mới) và phân phối lại các phần tử thành một mảng mới.Tóm lại, không đi sâu vào chi tiết cấu trúc của cây đỏ đen, điều sau sẽ xảy ra:
-
Phần tử đầu tiên của danh sách được viết dưới dạng gốc của toàn bộ cây (màu đen):
if (root == null) { x.parent = null; x.red = false; root = x; }
-
Đối với các yếu tố khác:
phân phối chúng sang trái hoặc phải tùy thuộc vào giá trị băm:
if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1;
Tất cả các nút con bên trái phải nhỏ hơn (hoặc bằng) nút gốc của chúng, trong khi tất cả các nút con bên phải phải lớn hơn.
-
Nếu hai phần tử có cùng giá trị băm và không thể so sánh theo bất kỳ cách nào khác (chúng không triển khai giao diện
Comparable
), chúng tôi sẽ gián đoạn việc xây dựng cây và gọi phương thứctieBreakOrder()
, do đó phương thức này sử dụng phương thức gốcSystem.identityHashCode()
để tính toán mã băm duy nhất trên toàn cầu .Xem thêm chi tiết tại đây: link bài viết
-
Chúng tôi kiểm tra các phần tử cây (đối tượng
TreeNode
) cho đến khi tìm thấy phần tử con (trái hoặc phải).if ((p = (dir <= 0) ? p.left : p.right) == null)
-
Thêm một nút con (trái hoặc phải tùy theo thư mục):
x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x;
-
Vì việc thêm một phần tử mới có thể làm mất cân bằng của cây nên chúng ta gọi phương thức tái cân bằng:
root = balanceInsertion(root, x);
Bạn có thể đọc về cách cân bằng CCH tại đây: habr
-
Sau khi cân bằng, phần tử gốc có thể thay đổi. Chúng tôi khắc phục điều này bằng cách gọi một phương thức đảm bảo rằng nút gốc được truyền tới nó sẽ là nút đầu tiên:
moveRootToFront(tab, root)
Bạn có thể thấy rõ cây đỏ đen được xây dựng và tự cân bằng như thế nào tại đây: trực quan
-
- Tính mã băm của khóa.
- Tính chỉ số xô.
- Đi qua chỉ mục và so sánh khóa của phần tử đầu tiên với giá trị hiện có. Nếu chúng bằng nhau, trả về giá trị, nếu không thì kiểm tra phần tử tiếp theo nếu nó tồn tại.
- Nếu đối tượng Node tiếp theo là null, trả về null.
- Nếu đối tượng Node tiếp theo không phải là null, hãy đi tới nó và lặp lại ba bước đầu tiên cho đến khi tìm thấy phần tử đó hoặc đối tượng Node tiếp theo là null.
get()
chúng ta nhận được giá trị cho khóa “KING”:
map.get("KING");
Bên trong, phương thức được gọi getNode(int hash, Object key)
, trong đó chính khóa (“KING”) và hàm băm của nó (2306996) được truyền vào, được tính toán trước theo cách tương tự như trong quá trình hoạt động put()
.
-
Chung ta kiểm tra:
-
bảng băm có tồn tại không:
(tab = table) != null
Hãy để tôi nhắc bạn rằng khi tạo HashMap, một mảng cho bảng không được tạo trong hàm tạo; điều này xảy ra sau trong phương thức
resize()
, phương thức này luôn được gọi khi thêm phần tử đầu tiên vào bảng băm. Do đó, nếu không có phần tử nào được thêm vào HashMap thì đơn giản là không có mảng bên trong để lưu trữ các phần tử. -
nếu biểu thức trước trả về true, bạn cần đảm bảo rằng độ dài của mảng bên trong lớn hơn 0:
(n = tab.length) > 0;
-
nếu biểu thức trước đó cũng trả về giá trị đúng, hãy đi tới nhóm tại chỉ mục (trong trường hợp 4 của chúng tôi), đã được tính toán trước đó và kiểm tra xem nó có xuất hiện các phần tử không:
(first = tab[(n - 1) & hash]) != null)
-
Chúng tôi so sánh khóa mà chúng tôi đang tìm kiếm với khóa của phần tử đầu tiên trong danh sách bên trong nhóm, vì trong hầu hết các nhóm sẽ chỉ có (không phải nơi nào chúng tôi có xung đột) một phần tử (trường hợp của chúng tôi). Như trong trường hợp của phương thức
put()
, các giá trị băm được so sánh và nếu chúng khớp nhau, các khóa sẽ được so sánh bằng tham chiếu và chỉ sau đó bằngequals()
.if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
Vì trong trường hợp của chúng ta, khóa “KING” sẽ đứng trước khóa “BLAKE” (trong danh sách liên kết, các phần tử mới được thêm vào cuối và KING được thêm vào trước), chúng ta sẽ dừng lại ở điểm này và trả về đối tượng đầu tiên của gõ Node vào phương thức get(), phương thức này sẽ “lấy” từ anh ta một trường có giá trị (100):
return (e = getNode(hash(key), key)) == null ? null : e.value;
-
-
Nếu có nhiều hơn một phần tử bên trong nhóm thì:
-
nếu nhóm là một danh sách được liên kết, chúng ta sẽ xem qua danh sách qua từng phần tử trong một vòng lặp
do – while
cho đến khi tìm thấy kết quả khớp:do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null);
-
nếu nhóm là cấu trúc cây thì phương thức này sẽ được gọi bổ sung
getTreeNode()
, phương thức này sẽ sử dụng phương thức này để tìm khóa được yêu cầufind()
. Chúng tôi thực hiện tìm kiếm dạng cây - các giá trị băm được so sánh và nút gốc bên trái hoặc bên phải để tìm kiếm được xác định. Nếu các khóa bằng nhau (theo tham chiếu hoặc theoequals
), hãy trả về nút này. Nếu nút con bên trái hoặc bên phải là null, chúng tôi sẽ so sánh thêm các khóa bằng cách sử dụng so sánh (nếu các khóa triển khai giao diệnComparable
), nếu không, chúng tôi thực hiện tìm kiếm đệ quy qua cây (cây con phải hoặc trái) cho đến khi tìm thấy kết quả khớp.
-
-
đi đến nhóm mong muốn (một lần nữa, nó được tính toán trước);
-
nếu chỉ có một đối tượng trong nhóm (chúng tôi kiểm tra con trỏ null của nó), chúng tôi sẽ so sánh các giá trị băm, liên kết và
equals
(nếu đột nhiên các giá trị băm không bằng nhau). Tìm thấy một trận đấu? Tuyệt vời, đây là khóa của chúng tôi - hãy xóa nó (=null) và trả về giá trị của khóa này. -
nếu có nhiều hơn một phần tử trong nhóm, chúng tôi sẽ kiểm tra từng phần tử trong một vòng lặp cho đến khi tìm thấy phần tử đó hoặc đến cuối danh sách.
-
nếu không tìm thấy phần tử, chúng tôi trả về null.
GO TO FULL VERSION