Углубленный анализ HashMap

Java задняя часть алгоритм Безопасность

Обзор

HashMap больше всего знаком тем, кто использует Java, и они используют его каждый день. На этот раз я в основном анализирую принцип работы HashMap, почему я возьму это дело для анализа, в основном это маленькие друзья, у которых недавно брали интервью, у которых спрашивали о HashMap, а знания, заложенные в HashMap, гораздо больше, чем положить и получить.

Почему он называется HashMap? Как это реализовано внутри? Большую часть времени он используется, String используется в качестве его ключа? Давайте взглянем на HashMap и подробно объясним вам эти проблемы.

Происхождение названия HashMap

На самом деле происхождение HashMap основано на технологии Hasing (Hasing).Хасинг заключается в преобразовании большой строки или любого объекта в небольшое значение, используемое для их представления.Эти более короткие значения можно легко использовать для облегчения индексации, для ускорить поиск.

Что такое хэш-карта

HashMap — это коллекция, используемая для хранения пар ключ-значение, вы можете использовать «ключ» для хранения данных. Когда вы хотите получить данные, вы можете получить данные через «ключ», каждая пара «ключ-значение» также называется Entry. Эти пары ключ-значение (Entry) разбросаны и хранятся в массиве, который является основой HashMap.

Как структура данных, HashMap используется для рутинных добавлений, удалений, модификаций и поиска, таких как массивы и связанные списки.Когда данные хранятся (помещаются), они не размещаются случайным образом, а сначала выделяется индекс (который можно понимать как «классификация») После выделения хранилища индекса время поиска может быть значительно сокращено при следующей выборке (получении). Мы знаем, что массивы очень эффективны при выполнении поиска и модификации, а добавления и удаления (не хвосты) неэффективны.В отличие от связанных списков, HashMap объединяет их.Давайте посмотрим на структуру данных HashMap.

Сначала введите переменные HashMap

size, это размер хранилища HashMap.thresholdЭто критическое значение HashMap, также называемое пороговым значением.Если HashMap достигает критического значения, размер необходимо перераспределить.loadFactorявляется коэффициентом загрузки и по умолчанию равен 75%. Порог = текущая длина массива ✖ коэффициент загрузки.modCountОтносится к общему количеству изменений или удалений HashMap.

Структура хранения HashMap



Запись разбросана и хранится в таблице-массиве типа Entry, и все данные в таблице являются объектом Entry. Направление оси Y представляет собой массив, а направление оси X — способ хранения связанного списка.

Тип Entry хранится в таблице, класс Entry содержит переменную хэш-кода, ключ, значение и другой объект Entry. Потому что это структура связанного списка. Найдите вас через меня, и вы найдете его. Однако Entry здесь не является LinkedList, это класс с внутренней структурой односвязного списка, который обслуживает только HashMap.

Характеристики массива заключаются в том, что запрос выполняется быстро, временная сложность — O(1), операции вставки и удаления — относительно медленные, а временная сложность — O(n). Способ хранения связанного списка несмежный, размер не фиксированный, характеристики противоположны массиву, вставка и удаление быстрые, скорость запроса медленная. HashMap обращается к ним и выбирает их сегменты, можно сказать, что операции запроса, вставки и удаления будут несколько ускорены.

Основы HashMap

1. Сначала определите, является ли ключ нулевым.Если он нулевой, найдите непосредственно Enrty[0].Если он не нулевой, сначала вычислите хэш-код ключа, а затем пройдите через два хэша, чтобы получить значение хэша.

2. По значению Hash взять остаток длины Entry[], и получить индекс массива Entry.

3. Найдите соответствующий массив по соответствующему индексу, то есть найдите связанный список, где он находится, а затем вставьте, удалите и запросите Значение в соответствии с операцией связанного списка.

Хэш-коллизия

хэш-метод

Все мы знаем, что у каждого объекта в Java есть метод hashcode(), который возвращает хэш-значение объекта. HashMap сначала выполняет хеш-операцию над хэш-кодом, а затем вычисляет нижний индекс через значение хеш-функции.

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
public final int hashCode() {
    return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}


Как HashMap находит индекс массива через Hash, вызов indexFor, где h — значение хеша, length — длина массива, этот алгоритм побитового И на самом деле составляет h%length остатка.

/**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

Где h — хеш-значение, а длина — длина массива.Этот алгоритм побитового И на самом деле является остатком h%length.

В общем, какая польза от этого алгоритма, типовая группировка. Например, как сгруппировать 100 чисел в 16 групп, вот что это значит. Применение очень широкое.

 static int indexFor(int h, int length) {
        return h & (length-1);
    }

Например

        int h=15,length=16;
        System.out.println(h & (length-1));
        System.out.println(Integer.parseInt("0001111", 2) & Integer.parseInt("0001111", 2));
        h=15+16;
        System.out.println(h & (length-1));
        System.out.println(Integer.parseInt("0011111", 2) & Integer.parseInt("0001111", 2));
        h=15+16+16;
        System.out.println(h & (length-1));
        System.out.println(Integer.parseInt("0111111", 2) & Integer.parseInt("0001111", 2));
        h=15+16+16+16;
        System.out.println(h & (length-1));
        System.out.println(Integer.parseInt("1111111", 2) & Integer.parseInt("0001111", 2));

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

При вызове метода put могут возникать коллизии, хотя мы стараемся избегать коллизий, чтобы повысить производительность HashMap. Говорят, что частота столкновений довольно высока, и столкновение начнется, когда средняя скорость загрузки достигнет 10%.

Анализ исходного кода

Инициализация HashMap

По умолчанию большинство людей звонят HashMap hashMap = new HashMap();Для инициализации мы анализируем здесь конструктор newHashMap (int initialCapacity, float loadFactor).

Все мы знаем, что у каждого объекта в Java есть метод hashcode(), который возвращает хэш-значение объекта. HashMap сначала выполняет хеш-операцию над хэш-кодом, а затем вычисляет нижний индекс через значение хеш-функции.

код показывает, как показано ниже:

public HashMap(int initialCapacity, float loadFactor) {
 // initialCapacity代表初始化HashMap的容量,它的最大容量是MAXIMUM_CAPACITY = 1 << 30。
 // loadFactor代表它的负载因子,默认是是DEFAULT_LOAD_FACTOR=0.75,用来计算threshold临界值的。    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

Из приведенного выше кода видно, что при инициализации необходимо знать инициализированную емкость, потому что индекс массива Entry будет вычисляться позже с помощью побитового алгоритма AND Hash, поэтому длина массива Entry должна быть от 2 до N-я власть.

поставить операцию

Как HashMap хранит объект?Код выглядит следующим образом:

public V put(K key, V value) {
        //数组为空时创建数组
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //①key为空单独对待
        if (key == null)
            return putForNullKey(value);
        //②根据key计算hash值
        int hash = hash(key);
        //②根据hash值和当前数组的长度计算在数组中的索引
        int i = indexFor(hash, table.length);
        //遍历整条链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //③hash值和key值都相同的情况,替换之前的值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                //返回被替换的值
                return oldValue;
            }
        }

        modCount++;
        //③如果没有找到key的hash相同的节点,直接存值或发生hash碰撞都走这
        addEntry(hash, key, value, i);
        return null;
    }

Как видно из кода, шаги следующие:

1. Во-первых, он определит, может ли он быть нулевым.Если он нулевой, вызовите pullForNullKey(value) для обработки. код показывает, как показано ниже:

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

Если значение ключа равно null, оно будет сохранено в связанном списке в начале таблицы [0] по умолчанию. Затем пройтись по каждой записи узла связанного списка таблицы [0], если обнаружено, что ключ записи узла равен нулю, заменить новое значение, а затем вернуть старое значение, если нет записи узла с ключом, равным null найден, добавьте новый узел.

2. Вычислите хэш-код ключа, а затем используйте вычисленный результат для двойного хеширования и найдите индекс i массива Entry с помощью indexFor(hash, table.length);.

(3) Затем пройдите по связанному списку с таблицей [i] в ​​качестве головного узла.Если вы найдете узел с тем же хешем и ключом, замените его новым значением, а затем верните старое значение.

Если узел с таким же хешем ключа не найден, добавьте новый узел addEntry(), код выглядит следующим образом:

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
        //判断数组容量是否足够,不足够扩容
            resize(2 * table.length);
    }

(4) Если размер HashMap превышает критическое значение, необходимо сбросить размер и расширить емкость, что будет объяснено позже.

Прилагается блок-схема.Эта картинка скопирована с других блоггеров.Я чувствую, что рисунок хорош.


получить операцию

Мы получаем сохраненное значение через hashMap.get(K key), значение ключа очень простое. Мы напрямую находим запись через индекс массива, а затем просматриваем запись, Когда хэш-код и ключ совпадают, это значение, которое мы изначально сохранили.

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

Вызовите getEntry(key) для получения записи, а затем верните значение записи, посмотрите на метод getEntry(key)

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

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

В методе get есть оценка null.Хеш-значение null всегда равно 0. В методе getNullKey(K key) он также ищется в соответствии с методом обхода.

Роль modCount

Как мы все знаем, HashMap не является потокобезопасным, но в некоторых приложениях с лучшей отказоустойчивостью, если вы не хотите страдать от накладных расходов на синхронизацию hashTable только из-за вероятности 1%, HashMap использует механизм Fail-Fast для Чтобы решить эту проблему, вы обнаружите, что modCount объявляется в исходном коде таким образом.

reSize

При вызове метода put, когда размер HashMap превышает критическое значение, необходимо расширить емкость HashMap. код показывает, как показано ниже:

    void resize(int newCapacity) { //传入新的容量
        //获取旧数组的引用
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        //极端情况,当前键值对数量已经达到最大
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //修改阀值为最大直接返回
            threshold = Integer.MAX_VALUE;
            return;
        }
        //步骤①根据容量创建新的数组
        Entry[] newTable = new Entry[newCapacity];
        //步骤②将键值对转移到新的数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //步骤③将新数组的引用赋给table
        table = newTable;
        //步骤④修改阀值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
Возвращает, если размер превышает максимальную емкость. В противном случае создайте новый массив Entry, длина которого вдвое превышает длину старого массива Entry. Затем скопируйте старую Entry[] в новую Entry[]. Код выглядит следующим образом:

    void transfer(Entry[] newTable, boolean rehash) {
        //获取新数组的长度
        int newCapacity = newTable.length;
        //遍历旧数组中的键值对
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //计算在新表中的索引,并到新数组中
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }



На данный момент я считаю, что у вас есть очень четкое представление о внутренностях HashMap.Если эта статья будет вам полезна, не забудьте поставить лайк и прокомментировать или подписаться на меня, я буду продолжать обновлять статью, спасибо за вашу поддержку!