Реализация HashMap Java 8

93

Согласно следующему документу ссылки: Реализация 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;
        }
    }
    //...
}

Как это используется? Мне просто нужно объяснение алгоритма .

Хаснайн Али Бора
источник

Ответы:

226

HashMapсодержит определенное количество ведер. Он используется, hashCodeчтобы определить, в какую корзину их поместить. Для простоты представьте его как модуль.

Если наш хэш-код - 123456 и у нас 4 сегмента, 123456 % 4 = 0значит, элемент попадает в первую корзину, Bucket 1.

HashMap

Если наша функция хэш-кода хороша, она должна обеспечивать равномерное распределение, поэтому все сегменты будут использоваться примерно одинаково. В этом случае корзина использует связанный список для хранения значений.

Связанные сегменты

Но нельзя полагаться на людей в реализации хороших хэш-функций. Люди часто пишут плохие хеш-функции, что приводит к неравномерному распределению. Также возможно, что нам просто не повезло с нашими вводами.

Плохая хэш-карта

Чем менее равномерно это распределение, тем дальше мы продвигаемся от операций O (1) и тем ближе мы приближаемся к операциям O (n).

Реализация Hashmap пытается смягчить это, организовывая некоторые сегменты в деревья, а не в связанные списки, если сегменты становятся слишком большими. Это то TREEIFY_THRESHOLD = 8, для чего. Если ведро содержит более восьми предметов, оно должно стать деревом.

Ковш для дерева

Это дерево - красно-черное дерево. Сначала он сортируется по хеш-коду. Если хэш-коды совпадают, он использует compareToметод, Comparableесли объекты реализуют этот интерфейс, иначе хэш-код идентификации.

Если записи удаляются с карты, количество записей в корзине может уменьшиться, так что эта древовидная структура больше не нужна. Вот для чего UNTREEIFY_THRESHOLD = 6это нужно. Если количество элементов в корзине становится меньше шести, мы можем вернуться к использованию связанного списка.

Наконец, есть файл MIN_TREEIFY_CAPACITY = 64.

Когда хэш-карта увеличивается в размере, она автоматически меняет размер, чтобы иметь больше сегментов. Если у нас есть небольшая хеш-карта, вероятность того, что мы получим очень полные корзины, довольно высока, потому что у нас не так много разных корзин, в которые можно было бы поместить материал. Намного лучше иметь большую хэш-карту с большим количеством менее заполненных корзин. Эта константа в основном говорит, что нельзя начинать превращать ведра в деревья, если наша хэш-карта очень маленькая - вместо этого следует изменить размер, чтобы стать больше.


Чтобы ответить на ваш вопрос о приросте производительности, эти оптимизации были добавлены для улучшения худшего случая. Я только предполагаю, но вы, вероятно, увидите заметное улучшение производительности из-за этих оптимизаций, только если бы ваша hashCodeфункция была не очень хорошей.

Майкл
источник
3
Нечетное распределение не всегда является признаком плохой хэш-функции. Некоторые типы данных, например String, имеют гораздо большее пространство значений, чем intхэш-код, поэтому столкновения неизбежны. Теперь это зависит от фактических значений, таких как фактические Strings, которые вы вводите в карту, получаете ли вы равномерное распределение или нет. Плохое распределение может быть результатом просто неудачи.
Хольгер
3
+1, я хотел бы добавить, что конкретный сценарий, который смягчает этот древовидный подход, - это DOS-атака с коллизией хэшей . java.lang.Stringимеет детерминированный, не криптографический hashCode, поэтому злоумышленники могут тривиально создавать отдельные строки с конфликтующими хэш-кодами. До этой оптимизации это могло ухудшить операции HashMap до O (n) -время, теперь оно просто снижает их до O (log (n)).
MikeFHay
1
+1, if the objects implement that interface, else the identity hash code.я искал эту еще часть.
Number945
1
@NateGlenn - хеш-код по умолчанию, если вы его не отменяете
Майкл
Я не понял: «Эта константа в основном говорит, что нельзя начинать превращать ведра в деревья, если наша хэш-карта очень маленькая - вместо этого она должна быть увеличена в размере». для MIN_TREEIFY_CAPACITY. Означает ли это: «Как только мы вставляем ключ, который должен быть хэширован, в корзину, уже содержащую 8 ( TREEIFY_THRESHOLD) ключей, и если в ней уже есть 64 ( MIN_TREEIFY_CAPACITY) ключа HashMap, связанный список этой корзины преобразуется в сбалансированное дерево».
анир
16

Проще говоря (насколько я мог проще) + еще несколько деталей.

Эти свойства зависят от множества внутренних вещей, которые было бы очень здорово понять, прежде чем переходить к ним напрямую.

TREEIFY_THRESHOLD -> когда отдельная корзина достигает этого значения (а общее число превышает MIN_TREEIFY_CAPACITY), оно преобразуется в идеально сбалансированный красный / черный узел дерева . Зачем? Из-за скорости поиска. Подумайте об этом по-другому:

для поиска записи в ведре / корзине с записями Integer.MAX_VALUE потребуется не более 32 шагов .

Некоторое вступление к следующей теме. Почему количество бункеров / ковшей всегда равно двойке ? По крайней мере, две причины: быстрее, чем операция по модулю, и по модулю на отрицательные числа будут отрицательными. И вы не можете поместить запись в "отрицательную" корзину:

 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 «шутят» как: быстро, быстро, быстро, медленно . Есть и другие реализации - поиск хеш-карты без пауз ...

Теперь UNTREEIFY_THRESHOLD вступает в игру после повторного хеширования. В этот момент некоторые записи могут перемещаться из этой корзины (n-1)&hashв другую (они добавляют еще один бит к вычислению - и, таким образом, могут перемещаться в другие корзины), и он может достичь этого UNTREEIFY_THRESHOLD. На этом этапе не red-black tree nodeстоит хранить корзину как , а как LinkedListвместо этого, например

 entry.next.next....

MIN_TREEIFY_CAPACITY - это минимальное количество сегментов перед преобразованием определенного сегмента в дерево.

Евгений
источник
10

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. Я попытался осветить это в своем ответе ...
Евгений
3

Вам нужно будет визуализировать это: скажем, есть ключ класса с переопределенной только функцией 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 разница становится более значительной.

арендованная радуга
источник
Он не может построить эффективное дерево, потому что у него нет другого способа сравнить ключи, кроме их хэш-кодов, которые все одинаковы, и их метода равенства, который не помогает с упорядочением.
user253751
@immibis Их хэш-коды не обязательно совпадают. Скорее всего, они разные. Если классы реализуют это, он дополнительно будет использовать compareTofrom Comparable. identityHashCodeэто еще один механизм, который он использует.
Майкл
@Michael В этом примере все хэш-коды обязательно одинаковы, и класс не реализует Comparable. identityHashCode будет бесполезен при поиске правильного узла.
user253751
@immibis Ах да, я только просмотрел это, но ты прав. Итак, как Keyне реализуем Comparable, identityHashCodeбудем использовать :)
Майкл
@EmonMishra, к сожалению, просто визуализации будет недостаточно, я попытался осветить это в своем ответе.
Евгений
2

Изменение в реализации HashMap было добавлено в JEP-180 . Целью было:

Повысьте производительность java.util.HashMap в условиях высокого хэш-коллизии, используя сбалансированные деревья, а не связанные списки для хранения записей карты. Реализуйте такое же улучшение в классе LinkedHashMap.

Однако чистая производительность - не единственное преимущество. Это также предотвратит атаку HashDoS , если для хранения пользовательского ввода используется хеш-карта, потому что красно-черное дерево , которое используется для хранения данных в корзине, имеет сложность вставки в худшем случае в O (log n). Дерево используется после выполнения определенных критериев - см . Ответ Евгения .

Антон Кроснев
источник
-1

Чтобы понять внутреннюю реализацию hashmap, вам нужно понять хеширование. Хеширование в простейшей форме - это способ присвоения уникального кода любой переменной / объекту после применения любой формулы / алгоритма к его свойствам.

Настоящая хеш-функция должна следовать этому правилу -

«Хеш-функция должна возвращать один и тот же хэш-код каждый раз, когда функция применяется к одним и тем же или равным объектам. Другими словами, два одинаковых объекта должны последовательно создавать один и тот же хэш-код ».

Авинаш
источник
Это не отвечает на вопрос.
Стивен С.