Внедрение HashMap Java 8
В соответствии со следующим документом ссылки: Реализация Java HashMap
Я запутался с реализацией HashMap
(вернее, улучшением в HashMap
). Мои запросы:
Во-первых,
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
Почему и как используются эти константы? Я хочу для этого несколько ясных примеров.
Как они достигают выигрыша в производительности с этим?
Во-вторых
Если вы видите исходный код HashMap
в JDK, вы найдете следующий статический внутренний класс:
static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
HashMap.TreeNode<K, V> parent;
HashMap.TreeNode<K, V> left;
HashMap.TreeNode<K, V> right;
HashMap.TreeNode<K, V> prev;
boolean red;
TreeNode(int arg0, K arg1, V arg2, HashMap.Node<K, V> arg3) {
super(arg0, arg1, arg2, arg3);
}
final HashMap.TreeNode<K, V> root() {
HashMap.TreeNode arg0 = this;
while (true) {
HashMap.TreeNode arg1 = arg0.parent;
if (arg0.parent == null) {
return arg0;
}
arg0 = arg1;
}
}
//...
}
Как он используется? Я просто хочу объяснить алгоритм.
Ответы
Ответ 1
HashMap
содержит определенное количество сегментов. Он использует hashCode
чтобы определить, в какое ведро их поместить. Для простоты представьте это как модуль.
Если наш хэш-код 123456 и у нас есть 4 сегмента, 123456 % 4 = 0
поэтому элемент помещается в первый блок, сегмент 1.
![HashMap]()
Если наша функция хеширования хороша, она должна обеспечивать равномерное распределение, поэтому все сегменты будут использоваться примерно одинаково. В этом случае корзина использует связанный список для хранения значений.
![Linked Buckets]()
Но вы не можете полагаться на людей для реализации хороших хэш-функций. Люди часто пишут плохие хэш-функции, что приводит к неравномерному распределению. Также возможно, что нам просто не повезло с нашими входами.
![Bad hashmap]()
Чем меньше это распределение, тем дальше мы переходим от операций O (1) и тем ближе мы продвигаемся к операциям O (n).
Реализация Hashmap пытается смягчить это путем организации некоторых блоков в деревья, а не связанных списков, если они становятся слишком большими. Это то, для чего TREEIFY_THRESHOLD = 8
. Если в ведре содержится более восьми предметов, оно должно стать деревом.
![Tree Bucket]()
Это дерево красно-чёрное. Сначала сортируется по хеш-коду. Если хеш-коды совпадают, он использует метод compareTo
Comparable
если объекты реализуют этот интерфейс, в противном случае хеш-код идентичности.
Если записи удаляются с карты, количество записей в корзине может уменьшиться, так что эта древовидная структура больше не нужна. Это то, для чего UNTREEIFY_THRESHOLD = 6
. Если количество элементов в корзине падает ниже шести, мы могли бы также вернуться к использованию связанного списка.
Наконец, есть MIN_TREEIFY_CAPACITY = 64
.
Когда хэш-карта увеличивается в размере, она автоматически изменяет свой размер, чтобы иметь больше блоков. Если у нас есть небольшая хэш-карта, вероятность того, что мы получим очень полные сегменты, достаточно высока, потому что у нас нет такого большого количества различных блоков, в которые можно помещать вещи. Намного лучше иметь большую хэш-карту с большим количеством менее заполненных блоков. Эта константа в основном говорит о том, что не нужно начинать делать сегменты в деревьях, если наша хэш-карта очень мала - вместо этого ее размер должен быть больше.
Чтобы ответить на ваш вопрос об увеличении производительности, эти улучшения были добавлены для улучшения наихудшего случая. Я только размышляю, но вы, вероятно, увидите только заметное улучшение производительности из-за этих оптимизаций, если ваша функция hashCode
была не очень хорошей.
Ответ 2
Проще говоря (насколько я мог бы проще) + еще несколько деталей.
Эти свойства зависят от множества внутренних вещей, которые было бы очень здорово понять, прежде чем перейти к ним напрямую.
TREEIFY_THRESHOLD → когда одна корзина достигает этого (а общее число превышает MIN_TREEIFY_CAPACITY
), она превращается в идеально сбалансированный узел красного/черного дерева. Зачем? Из-за скорости поиска. Подумайте об этом по-другому:
для поиска записи в корзине/корзине с записями Integer.MAX_VALUE потребуется не более 32 шагов.
Немного вступления к следующей теме. Почему количество бункеров/ведер всегда равно двум? По крайней мере, две причины: быстрее, чем операция по модулю и по отрицательным числам по модулю. И вы не можете поместить Entry в "отрицательное" ведро:
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<>();
В приведенном выше случае решение о том, куда идет запись, принимается только на основе последних 4 битов вашего хеш-кода.
Это где умножение ведер вступает в игру. При определенных условиях (объяснение в точных деталях займет много времени), объемы удваиваются. Зачем? Когда ведра удваиваются в размере, в игру вступает еще один бит.
Итак, у вас есть 16 сегментов - последние 4 бита хеш-кода определяют, куда идет запись. Вы удваиваете сегменты: 32 сегмента - 5 последних битов определяют, куда войдет запись.
Как таковой этот процесс называется повторным хэшированием. Это может стать медленным. То есть (для людей, которым это небезразлично), так как HashMap "шутит" как: быстро, быстро, быстро, slooow. Есть и другие реализации - поиск без паузы hashmap...
Теперь UNTREEIFY_THRESHOLD вступает в игру после повторного хеширования. В этот момент некоторые записи могут перемещаться из этих корзин в другие (они добавляют еще один бит к вычислению (n-1)&hash
- и, как таковые, могут перемещаться в другие корзины), и он может достигать этого UNTREEIFY_THRESHOLD
. На этом этапе не стоит сохранять корзину как red-black tree node
, а вместо этого использовать LinkedList
, например
entry.next.next....
MIN_TREEIFY_CAPACITY - это минимальное количество сегментов до того, как определенный сегмент трансформируется в дерево.
Ответ 3
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.
Ответ 4
Вам нужно будет визуализировать его: скажем, есть ключ класса с переопределенной функцией 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 разница становится более значимой.
Ответ 5
Изменение в реализации HashMap было добавлено с помощью JEP-180. Цель заключалась в следующем:
Повысьте производительность java.util.HashMap в условиях высокого хеш-столкновения, используя сбалансированные деревья, а не связанные списки для хранения записей в карте. Внедрите те же улучшения в классе LinkedHashMap
Однако чистая производительность - не единственный выигрыш. Он также будет предотвращать атаку HashDoS, если хэш-карта используется для хранения пользовательского ввода, потому что красно-черное дерево, используемое для хранения данных в ковше, имеет худшую сложность ввода в O (log n). Дерево используется после выполнения определенных критериев - см. ответ Евгения.
Ответ 6
Чтобы понять внутреннюю реализацию hashmap, вам нужно понять хеширование. Хеширование в простейшем виде - это способ присвоения уникального кода любой переменной/объекту после применения любой формулы/алгоритма к его свойствам.
Истинная хеш-функция должна следовать этому правилу -
"Хэш-функция должна возвращать один и тот же хэш-код каждый раз, когда функция применяется к одинаковым или равным объектам. Другими словами, два равных объекта должны последовательно создавать один и тот же хэш-код".