HashMap
содержит определенное количество ведер. Он используется, hashCode
чтобы определить, в какую корзину их поместить. Для простоты представьте его как модуль.
Если наш хэш-код - 123456 и у нас 4 сегмента, 123456 % 4 = 0
значит, элемент попадает в первую корзину, Bucket 1.
Если наша функция хэш-кода хороша, она должна обеспечивать равномерное распределение, поэтому все сегменты будут использоваться примерно одинаково. В этом случае корзина использует связанный список для хранения значений.
Но нельзя полагаться на людей в реализации хороших хэш-функций. Люди часто пишут плохие хеш-функции, что приводит к неравномерному распределению. Также возможно, что нам просто не повезло с нашими вводами.
Чем менее равномерно это распределение, тем дальше мы продвигаемся от операций O (1) и тем ближе мы приближаемся к операциям O (n).
Реализация Hashmap пытается смягчить это, организовывая некоторые сегменты в деревья, а не в связанные списки, если сегменты становятся слишком большими. Это то TREEIFY_THRESHOLD = 8
, для чего. Если ведро содержит более восьми предметов, оно должно стать деревом.
Это дерево - красно-черное дерево. Сначала он сортируется по хеш-коду. Если хэш-коды совпадают, он использует compareTo
метод, Comparable
если объекты реализуют этот интерфейс, иначе хэш-код идентификации.
Если записи удаляются с карты, количество записей в корзине может уменьшиться, так что эта древовидная структура больше не нужна. Вот для чего UNTREEIFY_THRESHOLD = 6
это нужно. Если количество элементов в корзине становится меньше шести, мы можем вернуться к использованию связанного списка.
Наконец, есть файл MIN_TREEIFY_CAPACITY = 64
.
Когда хэш-карта увеличивается в размере, она автоматически меняет размер, чтобы иметь больше сегментов. Если у нас есть небольшая хеш-карта, вероятность того, что мы получим очень полные корзины, довольно высока, потому что у нас не так много разных корзин, в которые можно было бы поместить материал. Намного лучше иметь большую хэш-карту с большим количеством менее заполненных корзин. Эта константа в основном говорит, что нельзя начинать превращать ведра в деревья, если наша хэш-карта очень маленькая - вместо этого следует изменить размер, чтобы стать больше.
Чтобы ответить на ваш вопрос о приросте производительности, эти оптимизации были добавлены для улучшения худшего случая. Я только предполагаю, но вы, вероятно, увидите заметное улучшение производительности из-за этих оптимизаций, только если бы ваша hashCode
функция была не очень хорошей.
String
, имеют гораздо большее пространство значений, чемint
хэш-код, поэтому столкновения неизбежны. Теперь это зависит от фактических значений, таких как фактическиеString
s, которые вы вводите в карту, получаете ли вы равномерное распределение или нет. Плохое распределение может быть результатом просто неудачи.java.lang.String
имеет детерминированный, не криптографическийhashCode
, поэтому злоумышленники могут тривиально создавать отдельные строки с конфликтующими хэш-кодами. До этой оптимизации это могло ухудшить операции HashMap до O (n) -время, теперь оно просто снижает их до O (log (n)).if the objects implement that interface, else the identity hash code.
я искал эту еще часть.MIN_TREEIFY_CAPACITY
. Означает ли это: «Как только мы вставляем ключ, который должен быть хэширован, в корзину, уже содержащую 8 (TREEIFY_THRESHOLD
) ключей, и если в ней уже есть 64 (MIN_TREEIFY_CAPACITY
) ключаHashMap
, связанный список этой корзины преобразуется в сбалансированное дерево».Проще говоря (насколько я мог проще) + еще несколько деталей.
Эти свойства зависят от множества внутренних вещей, которые было бы очень здорово понять, прежде чем переходить к ним напрямую.
TREEIFY_THRESHOLD -> когда отдельная корзина достигает этого значения (а общее число превышает
MIN_TREEIFY_CAPACITY
), оно преобразуется в идеально сбалансированный красный / черный узел дерева . Зачем? Из-за скорости поиска. Подумайте об этом по-другому:Некоторое вступление к следующей теме. Почему количество бункеров / ковшей всегда равно двойке ? По крайней мере, две причины: быстрее, чем операция по модулю, и по модулю на отрицательные числа будут отрицательными. И вы не можете поместить запись в "отрицательную" корзину:
int arrayIndex = hashCode % buckets; // will be negative buckets[arrayIndex] = Entry; // obviously will fail
Вместо этого вместо модуля используется приятный трюк:
(n - 1) & hash // n is the number of bins, hash - is the hash function of the key
Это семантически то же самое, что и операция по модулю. Он сохранит младшие биты. Это имеет интересные последствия, когда вы делаете:
Map<String, String> map = new HashMap<>();
Вот тут-то и играет роль умножение ведер. При определенных условиях (на объяснение точных деталей потребуется много времени ), ведра увеличиваются вдвое. Зачем? Когда ведра увеличиваются вдвое, в игру вступает еще один момент .
По сути, этот процесс называется повторным хешированием. Это может замедлиться. Это (для людей, которым не все равно), поскольку HashMap «шутят» как: быстро, быстро, быстро, медленно . Есть и другие реализации - поиск хеш-карты без пауз ...
Теперь UNTREEIFY_THRESHOLD вступает в игру после повторного хеширования. В этот момент некоторые записи могут перемещаться из этой корзины
(n-1)&hash
в другую (они добавляют еще один бит к вычислению - и, таким образом, могут перемещаться в другие корзины), и он может достичь этогоUNTREEIFY_THRESHOLD
. На этом этапе неred-black tree node
стоит хранить корзину как , а какLinkedList
вместо этого, напримерMIN_TREEIFY_CAPACITY - это минимальное количество сегментов перед преобразованием определенного сегмента в дерево.
источник
TreeNode
- альтернативный способ хранения записей, принадлежащих одной корзинеHashMap
. В более старых реализациях записи корзины хранились в связанном списке. В Java 8, если количество записей в корзине превышает пороговое значение (TREEIFY_THRESHOLD
), они сохраняются в древовидной структуре вместо исходного связанного списка. Это оптимизация.Из реализации:
/* * Implementation notes. * * This map usually acts as a binned (bucketed) hash table, but * when bins get too large, they are transformed into bins of * TreeNodes, each structured similarly to those in * java.util.TreeMap. Most methods try to use normal bins, but * relay to TreeNode methods when applicable (simply by checking * instanceof a node). Bins of TreeNodes may be traversed and * used like any others, but additionally support faster lookup * when overpopulated. However, since the vast majority of bins in * normal use are not overpopulated, checking for existence of * tree bins may be delayed in the course of table methods.
источник
TREEIFY_THRESHOLD
И, общее количество ящиков будет не менееMIN_TREEIFY_CAPACITY
. Я попытался осветить это в своем ответе ...Вам нужно будет визуализировать это: скажем, есть ключ класса с переопределенной только функцией hashCode (), чтобы всегда возвращать одно и то же значение
public class Key implements Comparable<Key>{ private String name; public Key (String name){ this.name = name; } @Override public int hashCode(){ return 1; } public String keyName(){ return this.name; } public int compareTo(Key key){ //returns a +ve or -ve integer } }
а затем в другом месте я вставляю 9 записей в HashMap, причем все ключи являются экземплярами этого класса. например
Map<Key, String> map = new HashMap<>(); Key key1 = new Key("key1"); map.put(key1, "one"); Key key2 = new Key("key2"); map.put(key2, "two"); Key key3 = new Key("key3"); map.put(key3, "three"); Key key4 = new Key("key4"); map.put(key4, "four"); Key key5 = new Key("key5"); map.put(key5, "five"); Key key6 = new Key("key6"); map.put(key6, "six"); Key key7 = new Key("key7"); map.put(key7, "seven"); Key key8 = new Key("key8"); map.put(key8, "eight"); //Since hascode is same, all entries will land into same bucket, lets call it bucket 1. upto here all entries in bucket 1 will be arranged in LinkedList structure e.g. key1 -> key2-> key3 -> ...so on. but when I insert one more entry Key key9 = new Key("key9"); map.put(key9, "nine"); threshold value of 8 will be reached and it will rearrange bucket1 entires into Tree (red-black) structure, replacing old linked list. e.g. key1 / \ key2 key3 / \ / \
Обход дерева выполняется быстрее {O (log n)}, чем LinkedList {O (n)}, и с увеличением n разница становится более значительной.
источник
compareTo
fromComparable
.identityHashCode
это еще один механизм, который он использует.Key
не реализуемComparable
,identityHashCode
будем использовать :)Изменение в реализации HashMap было добавлено в JEP-180 . Целью было:
Однако чистая производительность - не единственное преимущество. Это также предотвратит атаку HashDoS , если для хранения пользовательского ввода используется хеш-карта, потому что красно-черное дерево , которое используется для хранения данных в корзине, имеет сложность вставки в худшем случае в O (log n). Дерево используется после выполнения определенных критериев - см . Ответ Евгения .
источник
Чтобы понять внутреннюю реализацию hashmap, вам нужно понять хеширование. Хеширование в простейшей форме - это способ присвоения уникального кода любой переменной / объекту после применения любой формулы / алгоритма к его свойствам.
Настоящая хеш-функция должна следовать этому правилу -
«Хеш-функция должна возвращать один и тот же хэш-код каждый раз, когда функция применяется к одним и тем же или равным объектам. Другими словами, два одинаковых объекта должны последовательно создавать один и тот же хэш-код ».
источник