HashMapについて
この記事は主にHashMapの実現について書かれています。
HashMapとは何か、その使用方法、およびハッシュ方法に関する簡単な質問はスキップされます。
同時に、jdk1.8の前後では、HashMapはさまざまな方法とロジックで実装されているため、対応するメソッドロジックも異なります。この記事では、比較のために、後のjdk1.8に焦点を当てます。
同時に、この記事は主にソースコードの論理を把握し、最初にソースコードの論理を示し、前のスペースを論理表示として配置し、ソースコードとコメントを後ろに配置することで、習得が容易で、コンテキストが明確で、最初にロジックを明確にしてから、ソースコードの好みを組み合わせます。
HashMapの基本構造
まず、HashMapの最下層は配列であり、バージョンに関係なく、配列です。
transient Node<K,V>[] table;
内部には、ノードタイプの配列テーブルが含まれます。ノードはエントリから継承します(これについてはこれ以上ありません)
jdk1.8より前
は、HashMapの基本的な実装は配列+リンクリストでした。
- 配列は要素を格納するためのバケットとして使用されます。キーのハッシュ値からバケットを決定した後、対応するバケット(配列の位置)に要素を配置します。
- そうすると、当然、2つの異なるキーが同じハッシュコードを持ち、同じバケットに対応する2つの異なるハッシュコードが存在する可能性があります。それらのいずれかが可能な場合、ハッシュの競合が発生します。この時点で、リンクされています。問題を解決するためにリストが出てきます。
- バケット内で、ノードがリンクリストとして存在する場合、競合は発生しません
下の図に示すように
(https://blog.csdn.net/samniwu/article/details/90550196)
ただし、これにも問題があります。バケット内のリンクリストをクエリする場合、時間計算量はO(M)です。これは高すぎ、明らかにHashMapのおおよそのO(1)の概念に準拠していません。
そのため、jdk1.8の後に変更されました。
上記のリンクリストをリンクリスト/赤黒木構造に置き換えます。
これにより、バケット内のノードが多すぎる場合に、赤黒木(平衡二分探索木)を使用してクエリを実行し、時間の複雑さをO(M)からO(logM)に減らし、パフォーマンスを大幅に向上させます。 。
だから我々は今、種類の構造のハッシュマップは何を知っています。
HashMap内の重要なフィールドと属性
- テーブル:私たちの配列です
容量:これはフィールドではなく、実際には配列の長さですが、より重要なので、投稿しました- loadFactor:負荷係数、しきい値を計算するための係数。配列内のデータのまばらさを表すために使用できます。1に近いほど、密度が高く、0に近いほど、まばらです。
- しきい値:しきい値、拡張を判断するためのしきい値。しきい値=容量* loadFactor
ノード:
- リンクリストタイプノード
- ツリータイプノード:リンクリストから継承
ハッシュコードに基づいてバケットを決定する方法
最初にキーオブジェクトのハッシュメソッドに従ってハッシュコードを取得し
、次にそれを1回摂動させます(h = key.hashCode()) ^ (h >>> 16)
(上位16ビットは変更されないまま、下位16ビット^ =上位16ビット)。
摂動の目的は、回避するためにハッシュ値をさらにハッシュすることです。競合します。(>>>は右への論理シフトであり、上位ビットは0で埋められます)
、次にputValメソッド(putのコア)に渡され、
(n-1)とハッシュを使用して決定されます。どのバケットであるか
(ここでは、nが2 ^ nの場合、n-1のバイナリはすべて1であり、ハッシュ%n-1と同等であるため、各バケットでより均等に分散できます)
赤黒木を拡張して変換するタイミング
拡張
- サイズ> =容量* loadFactor:現在のHashMapの要素数がしきい値より大きい場合、容量を拡張する必要があります
- バケット内のリンクリストノードが8(デフォルト値)に達し、配列の長さが64に達しない場合
赤黒木を変換する
- バケット内のリンクリストノードが8(デフォルト値)に達し、配列の長さが64に達した場合:この時点で、配列の長さは十分に長く、現在のバケット内のノードが多すぎるため、次のことを行う必要があります。赤黒木に変換されます
HashMapの一般的なメソッドの論理分析
以下では、一般的なメソッドのソースコードロジックを分析して、HashMapがput操作を実行する方法、展開する方法、およびそれらを取得する方法を示します。
主に、put、get、およびresizeメソッドが含まれます。
put
putメソッドのコアはputValメソッドであるため、ここでは主にこのメソッドについて説明します。
putVal:
iftable数组为空,或者长度为0:
resize()
根据传入key定位到哪一个桶((n - 1) & hash)
if 桶是空的:
直接插入
else 桶不为空:
e = null
if桶中第一个元素和当前元素相同(桶相同且hash相同且key相同):
用e来记录这个节点
else if 桶中第一个元素是树节点:
调用红黑树插入方法插入方法(涉及到平衡):
if 存在一个节点,和当前插入元素相同:
e = 该节点
跳出当前方法
红黑树插入
else 桶中第一个元素是链表节点:
if 存在一个节点,和当前插入元素相同:
e = 该节点
跳出当前else
链表插入当前插入元素:
判断是否需要转换红黑树or扩容
if e!=null(当前插入元素已经存在):
新值换旧值
if 当前大小超过了阈值:
resize()
return
特定のロジックは上に示され、
ここに要約されています。
- 最初にバケットを見つける
- バケットは空で、直接挿入し、戻ります
- バケットが空ではありません。すでに存在するかどうかを確認し、存在する場合は交換します。
- そうでない場合は、追加します(リンクリストまたは赤黒木を追加します)
- 拡張する必要があるかどうかを判断します
getは
主にgetNodeメソッドを呼び出してノードを取得します
getNode:
if table为空 || table没有元素 || 对应的桶里没有元素:
return null
if 桶里第一个元素就和当前查询元素相同:
return 该节点
if 桶里不只一个节点:
if 节点类型是树:
return 红黑树查询的结果
if 节点类型是链表:
遍历找到节点
return 目标节点
サイズ変更方法
resize:
if 旧容量已经大于等于最大容量了:
将threshold置为Integer.MAX_VALUE
return 旧table (不更新)
else 没有超过最大容量:
新容量设置为之前的两倍
更新阈值
创建新的table数组
循环遍历旧table:
重新计算hash,确定桶
将元素插入桶里
return 新table
- 容量が以前の2倍に更新されました
- 新しい配列を作成し、ハッシュを再計算してから、コピーする必要があります
- サイズ変更は非常にリソースを消費します
ソースコードコメント
この部分のコードコメントはjavaGuideからのもので
あり、マークはなく、すべてjdk1.8です。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等;为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
//jdk1.7
public V put(K key, V value)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i); // 再插入
return null;
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 数组元素相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一个节点
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在链表中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({
"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}