HashMap为什么会是面试中的常客呢?我觉得有以下几点原因: * 考察你阅读源码的能力 * 是否了解内部数据结构 * 是否了解其存储和查询逻辑 * 对非线程安全情况下的使用考虑 Некоторое время назад коллега брал интервью у Ant Financial и ему задали этот вопрос.На самом деле, многие кейсы выводятся из связи между hashMap, hashTable и ConcurrentHahMap.Конечно, есть и прямые расследования по принципу hashMap. По сути, суть та же, просто проверить, понятны ли вам принципы, реализации и сценарии использования этих часто используемых коллекций в коллекциях. С одной стороны, мы его очень много используем в своей разработке, и конечно есть много людей, которые его используют, но не так много людей, которые используют его хорошо (я тоже много его использую, но не использую хорошо). Так что я воспользовался этой возможностью (принудительно растирая волну), чтобы еще раз взглянуть на этот HashMap.
Эта статья основана на jdk1.7.0_80, немного измененном после jdk 1.8, что будет подробно описано позже.
отношения наследования
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
hashMap реализует три интерфейса Map, Cloneable и Serializable и наследует абстрактный класс AbstractMap. hashTable наследует класс Dictionary, а также реализует три интерфейса: Map, Cloneable и Serializable.
главный атрибут
DEFAULT_INITIAL_CAPACITY начальная емкость по умолчанию 16 (хэш-таблица равна 11) константа
/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量-必须是2的幂。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
MAXIMUM_CAPACITY константа максимальной емкости по умолчанию
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*如果有一个更大的值被用于构造HashMap,则使用最大值
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
Коэффициент загрузки DEFAULT_LOAD_FACTOR (по умолчанию 0,75) постоянный
/**
* The load factor used when none specified in constructor.
* 加载因子,如果构造函数中没有指定,则使用默认的
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
EMPTY_TABLE пустая таблица по умолчанию
/**
* An empty table instance to share when the table is not inflated.
* 当表不膨胀时共享的空表实例。
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
table table, при необходимости измените размер. Длина должна быть степенью двойки.
Это также основная структура хранения в хэш-карте.
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
размер указывает количество KV, хранящихся в HashMap (сумма KV в связанном списке/дереве)
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
Переменная расширения порога указывает, что операция изменения размера будет выполнена, когда размер HashMap превысит пороговое значение.
порог=вместимость*коэффициент нагрузки
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;
Коэффициент загрузки loadFactor Коэффициент загрузки используется для измерения степени заполнения HashMap. Значение loadFactor по умолчанию — 0,75f. Метод расчета коэффициента загрузки HashMap в реальном времени: размер/емкость вместо деления количества занятых сегментов на емкость. (Последующее введение в концепцию ведер)
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
modCount
Количество раз, когда структура этого HashMap изменяется, это те, которые изменяют количество отображений в HashMap или изменяют его внутреннюю структуру (например, перефразируют). Это поле используется для быстрого отказа итераторов для представлений коллекций HashMap. (см. ConcurrentModificationException).
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
hashSeed Случайное значение, связанное с этим экземпляром, хеш-код для хеш-ключа, усложняющий поиск хеш-коллизий. Если 0, то альтернативное хеширование отключено.
/**
* A randomizing value associated with this instance that is applied to
* hash code of keys to make hash collisions harder to find. If 0 then
* alternative hashing is disabled.
*/
transient int hashSeed = 0;
Структурный анализ
static class Entry<K,V> implements Map.Entry<K,V>
Хэш-карта хранит каждое значение K-V с помощью статического внутреннего класса Entry, который наследуется от внутреннего класса Entry на карте. Посмотрите на конкретный код:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //键对象
V value; //值对象
Entry<K,V> next; //指向链表中下一个Entry对象,可为null,表示当前Entry对象在链表尾部
int hash; //键对象的hash值
/**
* 构造对象
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
/**
* 获取key
*/
public final K getKey() {
return key;
}
/**
* 获取value
*/
public final V getValue() {
return value;
}
/**
* 设置value,这里返回的是oldValue(这个不太明白,哪位大佬清楚的可以留言解释下,非常感谢)
*/
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/**
* 重写equals方法
*/
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
/**
* 重写hashCode方法
*/
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
HashMap — это коллекция, используемая для хранения пар ключ-значение, каждая пара ключ-значение также называется Entry. Эти пары ключ-значение (Entry) разбросаны и сохранены в массиве, который является основой HashMap (то есть таблица-сегмент выше).
Посмотрите на картинку:
Когда хэш-карта инициализируется, значение по умолчанию для каждого пробела равно нулю. При вставке элементов (специально вставленных в следующем анализе) конкретная позиция индекса вычисляется в соответствии со значением ключа. Если это повторяется, используется метод вставки хвоста. чтобы вставить его в следующий связанный список.
вставка хвоста Раньше я тестировал, вставляя 17 фрагментов данных (количество конкретных данных произвольно, чем больше число, тем выше вероятность повторения)
public static void main(String[] args) throws Exception {
HashMap<String, Object> map=new HashMap<>();
for (int i = 0; i < 170; i++) {
map.put("key"+i, i);
}
System.out.println(map);
}
Глядя на следующие контрольные точки, мы можем сделать выводы выше: 1. Для хранения конфликта индексов будет использоваться связанный список;
2. Способ вставки связанного списка — вставка из хвоста (официальное объяснение состоит в том, что обычно данные, вставленные позже, используются чаще), что способствует поиску.
основной метод
Наиболее часто используемый метод hashMap в нашей разработке заключается в том, чтобы сначала создать объект HashMap, затем сохранить его, а затем получить его; соответствующий метод:
Конструктор
поставить функцию
получить функцию
Конструктор
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity 指定的初始化容量大小
* @param loadFactor the load factor 指定的负载因子
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
//如果初始化容量小于0,则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始化容量大于最大容量,则使用默认最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果负载因子小于0或者非数值类型,则抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//初始化负载因子
this.loadFactor = loadFactor;
//初始化threshold
threshold = initialCapacity;
//这个初始化方法是个空方法,应该是意在HashMap的子类中由使用者自行重写该方法的具体实现
init();
}
Два других конструктора на самом деле являются вызовами вышеуказанного конструктора:
//只制定默认容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用HashMap默认的容量大小和负载因子
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
Другой:
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
Создает новую HashMap с тем же отношением сопоставления, что и указанная карта. HashMap создается с коэффициентом загрузки по умолчанию (0,75) и начальной емкостью, достаточной для размещения сопоставлений в указанной карте.
положить метод Во-первых, мы все знаем, что ключ в хэш-карте может быть нулевым, что также является наиболее часто задаваемым вопросом на собеседовании. Тогда позвольте мне сначала понять, почему значение null может быть сохранено в качестве значения ключа.
public V put(K key, V value) {
//如果table是空的
if (table == EMPTY_TABLE) {
//inflate:扩容/膨胀的意思
inflateTable(threshold);
}
//如果key为null 此处敲下桌子,为什么可以存null?
if (key == null)
//执行putForNullKey方法,这个方法的作用是如果key为null,就将当前的k-v存放到table[0],即第一个桶。
return putForNullKey(value);
//对key进行一次hash运算,获取hash值
int hash = hash(key);
//根据key值得hash值和表的长度来计算索引位置
int i = indexFor(hash, table.length);
//移动数据,插入数据
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
//上面Entry中的setValue中也有提到,返回的都是旧的数据
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
метод хеша:
Извлекает хэш-код объекта и применяет к полученному хэшу дополнительную хэш-функцию, которая защищает от хеш-функций низкого качества. Это важно, потому что HashMap использует хэш-таблицы двух длин, иначе вы столкнетесь с коллизиями хэш-кодов, которые неразличимы в более низком порядке. Примечание. Нулевые ключи всегда сопоставляются с хешем 0, поэтому индекс 0.
/**
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
//这个函数确保在每个比特位置上仅以恒定倍数不同
//的散列码具有有限数量的冲突(在默认加载因子下大约为8)。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
Описание конкретного процесса конфликта:
пустая таблица hashmap
Вставьте элемент, индекс равен 3 посредством вычисления хэша, потому что в текущей позиции 3 нет элемента, поэтому вы можете вставить его напрямую
Вставьте элемент снова и рассчитайте индекс с помощью хеш-вычисления или 3. Если есть конфликт, поместите вновь вставленный элемент в позицию исходного существующего элемента и укажите его рядом с исходным существующим элементом.получить метод Возвращает значение, которому сопоставлен указанный ключ, или null, если эта карта не содержит сопоставлений ключей.
public V get(Object key) {
//和存null key一样,取的时候也是从table[0]取
if (key == null)
return getForNullKey();
//获取entry
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
метод getEntry
final Entry<K,V> getEntry(Object key) {
//size等于0,说明当前hashMap中没有元素,直接返回null(每个entry默认值为null)
if (size == 0) {
return null;
}
//根据key值计算hash值
int hash = (key == null) ? 0 : hash(key);
//通过hash值获取到索引位置,找到对应的桶链进行遍历查找
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;
}
Механизм расширения
Как упоминалось выше, пороговое значение, переменная расширения, означает, что операция изменения размера будет выполнена, когда размер HashMap превысит пороговое значение. Его метод расчета: порог=мощность*коэффициент нагрузки.
Из приведенной выше формулы мы можем узнать, что время расширения хэш-карты равноКогда текущее значение текущего размера превышает емкость, умноженную на коэффициент загрузки, расширение будет запущено. Взгляните на исходный код:
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果当前size超过threshold 并且满足桶索引位置不为null的情况下,扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容之后为原来的两倍
resize(2 * table.length);
//重新计算hash值
hash = (null != key) ? hash(key) : 0;
//重写计算索引
bucketIndex = indexFor(hash, table.length);
}
//执行具体的插入操作
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//先取到当前桶的entry
Entry<K,V> e = table[bucketIndex];
//将新的数据插入到table[bucketIndex],再将之前的entry通过链表简介到table[bucketIndex]的next指向;前面的图已经进行了描述。
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
Следует отметить, что расширение не выполняется после заполнения хэш-карты, см. следующие точки останова:
Через конструктор по умолчанию создается новый объект карты, через цикл for вставляется 12 кусков данных, а точка останова находится в конце выполнения.Мы видим, что текущая емкость таблицы равна 16, а пороговое значение переменной раскрытия равно 12. (16x0,75).Теперь 12 меняем на 13.В настоящее время 13 все еще меньше 16, но расширение хэш-карты все еще срабатывает. Текущая емкость таблицы — 32 (увеличена в два раза по сравнению с предыдущим размером), а порог — 24 (32x0,75).Из этих двух диаграмм мы знаем:
Емкость после каждого расширения в два раза превышает первоначальную емкость (2n)
Запуск расширения происходит не из-за того, что текущий объект хэш-карты заполнен, а из-за того, что время запуска управляется пороговой переменной расширения.
резюме
Эта статья просто берет волну исходного кода и переводит комментарии в исходном коде в сочетании с моим собственным пониманием, а также кратко представляет применение хвостовой интерполяции в хэш-карте посредством отладки точек останова. Наконец, решение, когда возникает конфликт индексов в следующей хэш-карте, описано с помощью нескольких рисунков. Hashmap действительно глубокий и поверхностный в интервью, но все же необходимо прочитать исходный код Следующие два блога рекомендуются для всех.