static int indexFor(int h, int length) {
return h & (length-1);
}
作为输入,它采用工作结果获得的哈希码hashCode()
和内部数组的长度(单元数)。它返回结果“哈希码”->按位“AND”->(数组长度 - 1)。该类HashMap
继承自类AbstractMap
并实现以下接口:Map
、Cloneable
、Serializable
。.method 负责 Java 中的哈希函数hashCode()
。默认实现返回一个称为身份哈希码的hashCode()
值。即使类重写了,您也始终可以通过调用 来获取对象的 ID 哈希值。有时人们认为,OpenJDK 中的默认实现与内存地址无关。更多详细信息请参见:habr 在 HashMap 中,哈希表是基于单链表数组(或者更准确地说,动态的,因为表可以扩展)实现的。本质上,我们通过方法获得键的哈希码,然后对其进行修改(我们稍后将考虑),并在内部使用附加方法将结果值分发到所需的单元格。数组元素(单元)也称为存储桶,用于存储各个节点。每个桶都是一个集合(列表或树)。节点是嵌套类(或树结构中)的对象。事实上,数组单元内部只有一个单链表或红黑树,它是另一个类 - 的实现的基础。 hashCode()
System.identityHashCode(Object o)
hashCode()
hashCode()
Node
TreeNode
LinkedList
TreeMap
HashMap
包含以下字段:
final int hash
— 当前元素的哈希值,这是我们通过哈希键获得的结果;final K key
— 当前元素的键。这是您指定为方法中第一个对象的写入位置put()
;V value
— 当前元素的值。你在方法中指定的第二个对象写在这里put()
;Node < K, V> next
— 链接到同一篮子内的下一个节点。该列表是连接的,因此它需要一个不指向下一个节点的链接(如果有)。
transient Node < K, V> [] table
– 哈希表本身,基于数组实现,用于以节点的形式存储键值对。这是我们的节点存储的地方;transient int size
— 键值对的数量;int threshold
— 元素的最大数量,达到该数量后哈希表的大小将加倍。使用公式计算(容量*负载系数);final float loadFactor
— 该参数决定当前哈希表需要多大程度的负载才能创建新的哈希表,即 一旦哈希表已满 75%,就会创建一个新的哈希表,并将当前元素移入其中(这是一个成本高昂的操作,因为所有元素都必须重新哈希);transient Set< Map.Entry< K,V>> entrySet
- 包含一个缓存entrySet()
,我们可以用它进行迭代HashMap
。
static final int DEFAULT_INITIAL_CAPACITY= 1 << 4
— 默认哈希表容量(16);static final int MAXIMUM_CAPACITY = 1 << 30
— 哈希表的最大可能容量(大约 10 亿);static final float DEFAULT_LOAD_FACTOR = 0.75f
——默认负载系数;static final int TREEIFY_THRESHOLD = 8
是一个桶中元素数量的“阈值”,达到该阈值后,内部链表将转换为树结构(红黑树)。static final int UNTREEIFY_THRESHOLD = 6
— 如果一个篮子中的元素数量减少到 6,那么就会发生从树到链表的反向转换;static final int MIN_TREEIFY_CAPACITY = 64
— 可以转换为树结构的哈希表的最小容量(桶数)。那些。如果哈希表中至少有 64 个桶,并且一个桶中有 8 个或更多元素,那么就会发生向树结构的转变。
public HashMap()
— 默认创建哈希显示:体积(capacity) = 16
和负载因子(load factor) = 0.75
;public HashMap(Map< ? extends K, ? extends V> m)
— 创建一个由另一个给定映射的元素初始化的哈希映射,其初始容量足以容纳另一个映射的元素;public HashMap(int initialCapacity)
— 创建具有给定初始容量的哈希图。为了HashMap能够正确正确地工作,内部数组的大小必须是2的幂(即16、64、128等);public HashMap(int initialCapacity, float loadFactor)
— 使用指定参数创建哈希映射:初始容量和负载因子。
Map
并扩展类,AbstractMap
而不添加自己的方法。哈希映射不保证其元素的顺序。因此,元素进入哈希映射的顺序不一定是迭代器检索它们的顺序。 添加对象 添加键值对是使用put()
. 让我们看一下添加对象时涉及的步骤:
-
计算输入对象的键的哈希值。密钥哈希是通过
static final int hash(Object key)
已经访问hashCode()
我们已知的密钥方法的方法计算的。为此,hashCode()
可以使用重写方法或其默认实现。该方法中的附加转换hash()
:static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
Почему бы просто не вычислить с помощью
hashCode()
? Это сделано из-за того, чтоhashCode()
можно реализовать так, что только нижние битыint
'a будут заполнены. Например, дляInteger
,Float
– если мы в HashMap кладем маленькие значения, то у них и биты хеш-codeов будут заполнены только нижние. В таком случае ключи в HashMap будут иметь тенденцию скапливаться в нижних ячейках, а верхние будут оставаться пустыми, что не очень эффективно. На то, в Howой бакет попадёт новая запись, влияют только младшие биты хеша. Поэтому и придумали различными манипуляциями подмешивать старшие биты хеша в младшие, чтобы улучшить распределение по бакетам (чтобы старшие биты родного хеша an object начали вносить коррективы в то, в Howой бакет попадёт an object) и, How следствие, производительность. Потому и придумана дополнительная функцияhash
внутри HashMap. -
Вычисляем индекс бакета (ячейки массива), в который будет добавлен наш элемент:
i = (n - 1) & hash
где n – длина массива.
-
Создается an object Node.
-
Теперь, зная индекс в массиве, мы получаем список (цепочку) элементов, привязанных к этой ячейке. Если в бакете пусто, тогда просто размещаем в нем элемент. Иначе хэш и ключ нового element поочередно сравниваются с хешами и ключами элементов из списка и, при совпадении этих параметров, meaning element перезаписывается. Если совпадений не найдено, элемент добавляется в конец списка.
Теперь к очень подробному примеру.
-
Создадим an object класса HashMap:
HashMap < String, Integer> map = new HashMap<>();
-
С помощью метода
put()
добавим в хеш-отображение первую пару «ключ-meaning»:map.put("KING", 100);
В этот момент внутри вызывается метод
putVal()
. -
С помощью хеш-функции, роль которой играет метод hash, вычисляется хеш-code ключа, внутри которого предварительно вычисляется хеш-code с помощью метода hashCode() (в данном случае класса String), в результате чего мы получаем его «предварительное meaning» – 2306967. Может проверить в IDEA с помощью
System.out.println("KING".hashCode());
Полученный хеш-code модифицируется по формуле:
(хеш-code) ^ (хеш-code>>> 16)
, и в результате получаем окончательный хеш-code – 2306996. -
Проверяем таблицу на «пустоту»:
if ((tab = table) == null || (n = tab.length) == 0)
где
[] tab
— сама хеш-table: ссылаем tab и table (напомню, что это поле класса HashMap, которое хранит массив для наших элементов) на один и тот же массив, а int n – дополнительная переменная-счетчик.Так How проверка вернёт true (потому что массив для таблицы не был создан с помощью оператора new в конструкторе), будет вызван метод
resize()
, который собственно и создаст таблицу размером на 16 элементов. Да-да, конструкторы класса ниHowой таблицы не создают. Вместо этого всегда происходит вызов методаresize()
при первом добавлении element. Длина созданной таблицы (считай длина массива) будет записана в переменнуюn – n = (tab = resize()).length
, которая в дальнейшем используется для вычисления бакета. -
同时,我们计算将放置对象的桶的索引,并检查其中是否已经有元素。我们计算:
i = (n - 1) & hash i = (16 - 1) & 2306996 i = 4
查看:
if ((p = tab[i = (n - 1) & hash]) == null)
-
由于检查结果为 true(桶中没有元素),因此将生成具有以下字段的 Node 对象:
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
我们生成的 Node 对象将被添加到索引 [4] 下的存储桶中:
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
是一个仅返回 Node 类对象的方法。 -
添加后,将检查当前元素数量是否超过参数
threshold
:if (++size > threshold) resize();
如果超过,则会调用一个方法
resize()
来增加哈希表的大小。此时,该方法
putVal()
(分别为 和put()
)将完成其工作。结果可以用图形表示如下:
一般来说,如果所需的存储桶为空,这就是向哈希表添加节点(键值对)的情况。现在让我们尝试添加一个会导致碰撞的元素(当一个桶中有多个元素时)。
-
- 外部链接或链接方法(在 HashMap 中实现) - 即 单元格实际上包含一个列表(链)。并且列表可能已经包含多个值(不一定具有相同的哈希码)。
- 线性探测或开放寻址方法(在 IdentityHashMap 中实现)- 包括搜索哈希函数指向的空单元之后的第一个空单元;
-
使用该方法,
put()
我们将另一个键值对添加到哈希映射中:map.put("BLAKE", 10);
-
我们计算“初步哈希” - 63281361。我们修改它,结果我们得到最终的哈希码 - 63281940。
-
由于第一次检查“空”现在将返回 false(无需创建表),因此我们立即计算将放置对象的存储桶的索引:
i = (n - 1) & hash i = (16 - 1) & 63281940 i = 4
-
检查指定索引处的存储桶中是否存在元素,由于
if ((p = tab[i = (n - 1) & hash]) == null)
在这种情况下不满足条件(存储桶中已经有一个元素),因此我们转到块else
。 -
首先,我们将添加的元素与桶内链表的第一个元素进行比较:
(p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
При проверке сначала сравниваются хеши ключей. Если этот «участок»
(p.hash == hash)
возвращает false, тогда остальная часть условия игнорируется (&&
), так How an objectы гарантированно разные. Иначе затем сравниваются ключи по ссылке (==) и в случае неequalsства, ключи сравниваются уже посредством методаequals()
. Сравнение осуществляется в таком порядке во благо производительности. Если все три выражения возвращают true, это означает, что ключи равны и новый элемент будет записан в дополнительную переменную, чтобы в дальнейшем с её помощью перезаписать meaning у ключа:if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
В результате сравнения ключей мы получаем false уже на первом этапе (разные хеши).
-
Игнорируем condition (
p instanceof TreeNode
), так How структура в бакете не является древовидной на данном этапе. -
Далее переходим в цикл
for
, где в пределах одного бакета проверяем у элементов указатель на следующий элементnext
, и если он equals null (значит элемент в списке последний и единственный), добавляем новый элемент Node в конец списка:if ((e = p.next) == null){ p.next = newNode(hash, key, value, null) ... };
Вы можете спросить, а где же проверка на equalsство ключей? Мы же не можем просто взять и добавить новый элемент. Так вот она уже была заранее осуществлена в пункте (5). Благодаря этому, теперь мы можем просто проверить указатель этого element, и так How он сейчас equals null, можно сделать вывод о том, что в списке только один элемент. И так How мы уже проверяли их ключи, мы можем безболезненно добавлять новый элемент.
Если же при первой итерации указатель не equals null, это говорит о том, что в списке How минимум два element, поэтому в таком случае мы переходим к следующему условия и сравниваем ключ добавляемого element со всеми ключами элементов в списке (способом, описанным в пятом пункте).
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
Если при сравнении ключей будет найдено совпадение, новый элемент будет записан в дополнительную переменную, чтобы в дальнейшем с её помощью перезаписать meaning у ключа.
После добавления второго element наш an object HashMap графически выглядит так:
В итоге, добавление элементов при коллизии (в пределах одного бакета) выглядит следующим образом:
- проверяем с помощью методов
hashCode()
иequals()
, что оба ключа одинаковы. - если ключи одинаковы, заменить текущее meaning новым.
- иначе связать новый и старый an objectы с помощью структуры данных "связный список", указав ссылку на следующий an object в текущем и сохранить оба под нужным индексом; либо осуществить переход к древовидной структуре
- проверяем с помощью методов
-
После каждой итерации (добавления нового element) в цикле for увеличивается счетчик, который отвечает за количество элементов в бакете:
for (int binCount = 0; ; ++binCount)
До тех пор, пока их количество не станет равно or больше 7:
binCount >= TREEIFY_THRESHOLD – 1
В таком случае произойдет вызов метода
treeifyBin()treeify()
для непосредственного построения древовидной структуры. Однако, если количество бакетов в текущей хеш-таблице меньше 64:if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
将调用一个方法来
resize()
增加哈希表的大小,重新分配元素,而不是进入树结构。treeify()
反过来,链表转换TreeNode
成红黑树。该方法resize()
迭代当前存储的所有元素,重新计算它们的索引(考虑到新的大小)并将元素重新分配到新数组中。简而言之,在不深入红黑树结构细节的情况下,会发生以下情况:
-
列表的第一个元素被写为整个树的根(黑色):
if (root == null) { x.parent = null; x.red = false; root = x; }
-
对于其他元素:
根据哈希值将它们分布到左侧或右侧:
if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1;
所有左子节点必须小于(或等于)其根节点,而所有右子节点必须大于其根节点。
-
如果两个元素具有相同的哈希值并且无法以任何其他方式进行比较(它们没有实现接口
Comparable
),我们会中断树的构造并调用该方法tieBreakOrder()
,该方法又使用本机方法System.identityHashCode()
来计算全局唯一的哈希码。更多详细信息请参见:文章链接
-
我们检查树元素(对象
TreeNode
),直到找到子(左或右)零元素。if ((p = (dir <= 0) ? p.left : p.right) == null)
-
添加一个子节点(左或右取决于目录):
x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x;
-
由于添加新元素可能会破坏树的平衡,因此我们将方法称为重新平衡:
root = balanceInsertion(root, x);
您可以在这里阅读有关平衡 CCH 的信息:habr
-
平衡后,根元素可能会发生变化。我们通过调用一个方法来修复这个问题,该方法保证传递给它的根将是第一个节点:
moveRootToFront(tab, root)
在这里可以清楚地看到红黑树是如何构建和自平衡的:可视化
-
- 计算密钥的哈希码。
- 计算桶索引。
- 遍历索引并将第一个元素的键与现有值进行比较。如果它们相等,则返回该值,否则检查下一个元素(如果存在)。
- 如果下一个 Node 对象为 null,则返回 null。
- 如果下一个Node对象不为null,则转到它并重复前三个步骤,直到找到该元素或者下一个Node对象为null。
get()
我们获取“KING”键的值:
map.get("KING");
在内部,调用该方法getNode(int hash, Object key)
,将密钥本身(“KING”)及其哈希值(2306996)传递给该方法,该哈希值以与操作期间相同的方式预先计算put()
。
-
我们检查:
-
哈希表是否存在:
(tab = table) != null
让我提醒您,创建 HashMap 时,不会在构造函数中创建表的数组;这会在稍后的方法中创建
resize()
,该方法在将第一个元素添加到哈希表时总是被调用。因此,如果HashMap中还没有添加任何元素,那么根本就没有内部数组来存储这些元素。 -
如果前面的表达式返回 true,则需要确保内部数组的长度大于 0:
(n = tab.length) > 0;
-
如果前面的表达式也返回 true,则转到之前计算的索引(在我们的例子中为 4)处的存储桶,并检查它是否存在元素:
(first = tab[(n - 1) & hash]) != null)
-
我们将要查找的键与存储桶内列表中第一个元素的键进行比较,因为在大多数存储桶中(并非在所有发生冲突的地方)只有一个元素(我们的情况)。与方法 的情况一样
put()
,会比较哈希值,如果它们匹配,则通过引用比较键,然后才通过 进行比较equals()
。if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
因为在我们的例子中,键“KING”将在键“BLAKE”之前(在链表中,新元素被添加到末尾,KING首先被添加),我们将在此时停止并返回第一个对象在 get() 方法中输入 Node,该方法会从他那里“抢走”一个值为 (100) 的字段:
return (e = getNode(hash(key), key)) == null ? null : e.value;
-
-
如果桶内有多个元素,则:
-
如果桶是一个链表,我们会循环遍历列表中的每个元素,
do – while
直到找到匹配项:do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null);
-
如果桶是树结构,则另外调用该方法
getTreeNode()
,进而使用该方法来查找所需的键find()
。我们进行树搜索 - 比较哈希值并确定要搜索的左根节点或右根节点。如果键相等(通过引用或通过equals
),则返回此节点。如果左或右子节点为空,我们还使用compareTo比较键(如果键实现接口Comparable
),否则我们在树(右子树或左子树)中执行递归搜索,直到找到匹配项。
-
-
转到所需的存储桶(同样,它是预先计算的);
-
如果桶中只有一个对象(我们检查其空指针),我们会比较哈希值、链接和
equals
(如果突然哈希值不相等)。找到匹配的了吗?太棒了,这是我们的键 - 删除它(=null)并返回该键的值。 -
如果桶中有多个元素,我们会循环检查每个元素,直到找到该元素或到达列表末尾。
-
如果没有找到该元素,我们返回 null。
GO TO FULL VERSION