みなさん、こんにちは。GuYanです。今日は、ほとんどすべてのインタビュアーがインタビュー中に提起する質問について話したいと思います-HashMapの拡張メカニズムは何ですか?私もプログラミングを学んでいる初心者なので、このブログ投稿では複数のブログ投稿を参照し、最後にまとめます。
このブログ投稿では、JDK1.8より前のHashMap拡張メカニズムのみを紹介しています。JDK1.8ではHashMapに赤黒ツリーの概念が導入されているため、この記事の範囲を超えているため、ここでは説明しません。
HashMap拡張メカニズム
サイズ変更とは何ですか?
サイズ変更:容量を再計算し、要素をHashMapオブジェクトに継続的に追加します。また、HashMapオブジェクト内の配列がそれ以上要素をロードできない場合、オブジェクトは配列の長さを拡張して、より多くの要素をロードできるようにする必要があります。 。もちろん、Javaの配列は自動的に拡張できません。方法は、水を貯めるために小さなバケツを使用するのと同じように、新しい配列を使用して既存の配列を小さな容量に置き換えることです。より多くの水を貯蔵したい場合は、大きなバケツを変更する必要があります。 。
いつ拡張するのですか?
コンテナに要素を追加すると、現在のコンテナの要素数が判断されます。現在のコンテナの要素数がしきい値(しきい値)以上の場合、つまり、現在のコンテナの要素数が現在の配列の長さに負荷係数を掛けた値よりも大きい場合は、自動的に処理されます。拡張されました。
拡張のプロセス!
以下では、ソースコード+画像+テキストの説明を使用して、HashMapの拡張プロセスを紹介します。
/**
* HashMap 添加节点
*
* @param hash 当前key生成的hashcode
* @param key 要添加到 HashMap 的key
* @param value 要添加到 HashMap 的value
* @param bucketIndex 桶,也就是这个要添加 HashMap 里的这个数据对应到数组的位置下标
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值
// 2.底层数组的bucketIndex坐标处不等于null
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//扩容之后,数组长度变了
hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢?
bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。
}
createEntry(hash, key, value, bucketIndex);
}
/**
* 这地方就是链表出现的地方,有2种情况
* 1,原来的桶bucketIndex处是没值的,那么就不会有链表出来啦
* 2,原来这地方有值,那么根据Entry的构造函数,把新传进来的key-value mapping放在数组上,原来的就挂在这个新来的next属性上了
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K, V> e = table[bucketIndex];
table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);
size++;
}
上記のaddEntryメソッドでは、サイズ(現在のコンテナー内の要素の数)がしきい値(配列の長さに負荷係数を掛けたもの)以上であり、基になる配列のbucketIndex座標がnullに等しくない場合、サイズ変更が実行されます。それ以外の場合、拡張はありません。
以下では、拡張プロセスに焦点を当てます。
void resize(int newCapacity) {
//传入新的容量
Entry[] oldTable = table; //引用扩容前的Entry数组
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
//扩容前的数组大小如果已经达到最大(2^30)了
threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
transfer(newTable); //!!将数据转移到新的Entry数组里
table = newTable; //HashMap的table属性引用新的Entry数组
threshold = (int) (newCapacity * loadFactor);//修改阈值
}
拡張する前に、まず拡張前の配列の参照アドレスを取得してoldTable変数に格納し、次に拡張前の配列の長さがintタイプに格納されている最大値に達しているかどうかを判断します。到達している場合は、配列容量が最大に達しており、拡張できないため、拡張を中止します。アップ。
次の図は、プログラムがEntry [] newTable = new Entry [newCapacity];コードを実行した後の状態を示しています。
ここでは、既存の小さい容量の配列を置き換えるために大きい容量の配列を使用します。transfer ()メソッドは元のEntry配列になります。の要素が新しいエントリ配列にコピーされます。
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
//遍历旧的Entry数组
Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry<K, V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //标记[1]
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
}
static int indexFor(int h, int length) {
return h & (length - 1);
}
newTable [i]の参照は、e.nextに割り当てられます。つまり、単一リンクリストのヘッド挿入方法が使用されます。同じ位置にある新しい要素は常にリンクリストの先頭に配置されるため、インデックスに配置された要素は最初に終了します。エントリチェーンの最後に配置されます(ハッシュの競合がある場合)。古い配列の同じエントリチェーン上の要素は、インデックス位置を再計算した後、新しい配列の異なる位置に配置される場合があります。
以下は、画像の形で転送プロセスを示しています(下の画像の赤いフォントは上の画像との違いを示しています。次の画像はすべてこのようなものです。赤いフォントの説明は繰り返されません)
次の図は、プログラムが実行された後の状態を示していますsrc [j] = null;(これは最初のループの状態です):
まず、table []配列の参照アドレスをsrc []配列に割り当てます。
次に、Entry <K、V> e = src [j];は、src [j]のリンクされたリストをe変数に転送して保存します。src [j]のリンクリストはeに渡されて保存されているので、大胆にsrc [j] = null;に設定して、ガベージコレクションを待つことができます。
次の図は、プログラムがEntry <K、V> next = e.next;を実行した後の状態を示しています(これは最初のループの状態です)。
ここで、e.nextの値は次の変数にバックアップされ、後続のコードはe.nextのポイントを変更するため、e.nextの値はここにバックアップされます。
次の図は、プログラムが実行された後の状態を示していますe.next = newTable [i];(これは最初のループの状態です):
上図に示すように、newTable [3]の値がnullであるため、e.nextはnullです。
次の図は、プログラムがnewTable [i] = e;コードを実行した後の状態を示しています(これは最初のサイクルの状態です)。
次の図は、プログラムがe = next;コードを実行した後の状態を示しています(これは最初のループの状態です):
上記のように、エントリ1ノードがnewTableに正常に挿入されました。ループの終わりで、e!と判断されます。 = nullであるため、すべてのノードがnewTableに移動されるまで、上記のプロセスが再度繰り返されます。
概要
- 拡張は特にパフォーマンスを消費する操作であるため、プログラマーがHashMapを使用する場合は、マップのサイズを見積もり、初期化中に大まかな値を指定して、マップが頻繁に拡張されないようにします。
- 負荷係数は変更することも、1より大きい場合もありますが、特別な状況でない限り、簡単に変更しないことをお勧めします。
- HashMapはスレッドセーフではありません。並行環境でHashMapを同時に操作しないでください。ConcurrentHashMapを使用することをお勧めします。
- JDK1.8に赤黒の木が導入されたことで、HashMapのパフォーマンスが大幅に最適化されました。