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
— 現在の要素の値。そして、メソッドの 2 番目のオブジェクトとして指定した内容がここに書き込まれますput()
。Node < K, V> next
— 同じバスケット内の次のノードへのリンク。リストは接続されているため、次のノードがある場合は、それへのリンクは必要ありません。
transient Node < K, V> [] table
– キーと値のペアをノードの形式で保存するための、配列に基づいて実装されたハッシュ テーブル自体。ここにノードが保存されます。transient int size
— キーと値のペアの数。int threshold
— 要素の最大数。これに達すると、ハッシュ テーブルのサイズが 2 倍になります。式 (容量 * 負荷係数) を使用して計算されます。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
1 つのバケット内の要素数の「しきい値」です。このしきい値に達すると、内部リンク リストがツリー構造 (赤黒ツリー) に変換されます。static final int UNTREEIFY_THRESHOLD = 6
— 1 つのバスケット内の要素の数が 6 に減少すると、ツリーからリンク リストへの逆遷移が発生します。static final int MIN_TREEIFY_CAPACITY = 64
— ツリー構造への遷移が可能なハッシュテーブルの最小容量(バケット数)。それらの。ハッシュ テーブルに少なくとも 64 個のバケットがあり、1 つのバケットに 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()
) は作業を完了します。結果は次のようにグラフで表すことができます。
一般に、目的のバケットが空の場合にハッシュ テーブルにノード (キーと値のペア) を追加すると、次のようになります。次に、衝突を引き起こす要素を追加してみましょう (1 つのバケットに複数の要素がある場合)。
-
- 外部チェーンまたはチェーン メソッド (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;
左側のすべての子はルート ノード以下 (または同等) である必要があり、右側のすべての子はそれより大きくなければなりません。
-
2 つの要素が同じハッシュを持ち、他の方法で比較できない場合 (インターフェイスが実装されていない場合
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 になるまで最初の 3 つの手順を繰り返します。
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)
-
探しているキーをバケット内のリストの最初の要素のキーと比較します。これは、ほとんどのバケットでは (衝突が発生するすべての場所ではありません) 要素が 1 つしかないためです (今回の場合)。メソッドの場合と同様に
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
)、このノードを返します。左または右の子ノードが null の場合は、さらに CompareTo を使用してキーを比較します (キーがインターフェースを実装している場合Comparable
)。それ以外の場合は、一致するものが見つかるまでツリー (右または左のサブツリー) を介して再帰的検索を実行します。
-
-
目的のバケットに移動します (これも事前に計算されています)。
-
バケット内にオブジェクトが 1 つだけある場合 (ヌル ポインタを確認します)、ハッシュ、リンク、および
equals
(突然ハッシュが等しくない場合) を比較します。一致するものが見つかりましたか? これは私たちのキーです。削除して (=null)、このキーの値を返します。 -
バケット内に複数の要素がある場合は、要素が見つかるかリストの最後に到達するまで、ループで各要素をチェックします。
-
要素が見つからなかった場合は、null を返します。
GO TO FULL VERSION