ホームページ Java &#&チュートリアル JavaにおけるHashMapの実装原理の解析

JavaにおけるHashMapの実装原理の解析

Sep 11, 2018 pm 01:57 PM
hashmap

この記事の内容は Java における HashMap の実装原理の分析に関するもので、必要な方は参考にしていただければ幸いです。

1. ハッシュマップの概要:
HashMap は、ハッシュ テーブルに基づく Map インターフェイスの非同期実装です。この実装では、すべてのオプションのマッピング操作が提供され、null 値と null キーが許可されます。このクラスはマッピングの順序を保証せず、特に順序が不変であることを保証しません。
2. HashMap データ構造:
Java プログラミング言語には、2 つの最も基本的な構造があり、1 つは配列、もう 1 つはシミュレートされたポインター (参照) であり、これら 2 つの基本構造を使用してすべてのデータ構造を構築できます。HashMap も例外ではありません。 HashMap は実際には、配列とリンク リストを組み合わせた「リンク リスト ハッシュ」データ構造です。
上の図からわかるように、HashMap の最下層は配列構造であり、配列内の各項目はリンクされたリストです。新しい HashMap が作成されると、配列が初期化されます。

/** 
 * The table, resized as necessary. Length MUST Always be a power of two. 
 */  transient Entry[] table;  

static class Entry<K,V> implements Map.Entry<K,V> {  
    final K key;  
    V value;  
    Entry<K,V> next;  
    final int hash;  
    ……  
}
ログイン後にコピー

3. HashMap アクセスの実装:
1) ストレージ:

public V put(K key, V value) {  
    // HashMap允许存放null键和null值。  
    // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  
    if (key == null)  
        return putForNullKey(value);  
    // 根据key的keyCode重新计算hash值。  
    int hash = hash(key.hashCode());  
    // 搜索指定hash值在对应table中的索引。  
    int i = indexFor(hash, table.length);  
    // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。  
    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;  
        }  
    }  
    // 如果i索引处的Entry为null,表明此处还没有Entry。  
    modCount++;  
    // 将key、value添加到i索引处。  
    addEntry(hash, key, value, i);  
    return null;  
}
ログイン後にコピー

上記のソース コードからわかるように、HashMap に要素を配置すると、まずキーの hashCode に基づいてハッシュ値を再計算し、配列内の要素の位置を取得します。ハッシュ値 (つまり添え字) に基づいて、配列内のこの位置に他の要素が格納されている場合、この位置の要素はリンク リストの形式で格納され、新しく追加された要素がリストの先頭に配置されます。チェーンと、チェーンの最後に配置される最初に追加された要素。配列内のその位置に要素がない場合、要素は配列内のその位置に直接配置されます。
addEntry(hash, key, value, i) メソッドは、計算されたハッシュ値に基づいて、配列テーブルの i インデックスにキーと値のペアを配置します。 addEntry は、HashMap によって提供されるパッケージ アクセス メソッドです。コードは次のとおりです。

void addEntry(int hash, K key, V value, int bucketIndex) {  
    // 获取指定 bucketIndex 索引处的 Entry   
    Entry<K,V> e = table[bucketIndex];  
    // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    // 如果 Map 中的 key-value 对的数量超过了极限  
    if (size++ >= threshold)  
    // 把 table 对象的长度扩充到原来的2倍。  
        resize(2 * table.length);  
}
ログイン後にコピー

システムが HashMap にキーと値のペアを格納することを決定するとき、エントリ内の値はまったく考慮されず、単に計算されます。キーに基づいて各エントリを決定します。 Map コレクションの値は、システムがキーの保存場所を決定した後、そこに保存できるようになり、キーの付属品として完全に考えることができます。

hash(int h) メソッドは、キーの hashCode に基づいてハッシュを再計算します。このアルゴリズムでは、上位ビットの計算が追加され、下位ビットが変更されず上位ビットが変更された場合に発生するハッシュの競合を防ぎます。

static int hash(int h) {  
    h ^= (h >>> 20) ^ (h >>> 12);  
    return h ^ (h >>> 7) ^ (h >>> 4);  
}
ログイン後にコピー

HashMap 内の要素を見つけるには、キーのハッシュ値に基づいて、対応する配列内の位置を見つける必要があることがわかります。この位置を計算する方法はハッシュ アルゴリズムです。前述したように、HashMap のデータ構造は配列とリンク リストの組み合わせであるため、当然のことながら、この HashMap 内の要素の位置ができるだけ均等に分散され、各位置に要素が 1 つだけ存在することが望まれます。ハッシュ アルゴリズムを使用してこの位置を取得すると、リンク リストをたどることなく、対応する位置にある要素が必要なものであることがすぐにわかり、クエリの効率が大幅に最適化されます。

どのオブジェクトでも、その hashCode() 戻り値が同じである限り、 hash(int h) メソッドを呼び出すプログラムによって計算されるハッシュ コード値は常に同じです。最初に考えられるのは、要素の分布が比較的均一になるように、配列の長さを法としてハッシュ値を取得することです。ただし、「モジュロ」演算の消費量は依然として比較的多く、これは HashMap で行われます。indexFor(int h, int length) メソッドを呼び出して、オブジェクトを保存するテーブル配列のインデックスを計算します。 IndexFor(int h, int length) メソッドのコードは次のとおりです:

static int indexFor(int h, int length) {  
    return h & (length-1);  
}
ログイン後にコピー

このメソッドは、h & (table.length -1) を使用してオブジェクトのストレージ ビットと長さを取得します。 HashMap の基礎となる配列は常に 2 の n 乗です。これが HashMap の速度の最適化です。 HashMap コンストラクターには次のコードがあります:

int capacity = 1;  
    while (capacity < initialCapacity)  
        capacity <<= 1;
ログイン後にコピー

このコードは、初期化中に HashMap の容量が常に 2 の n 乗であること、つまり、基になる配列の長さが常に 2 の n 乗であることを保証します。
長さが常に 2 の n 乗である場合、h& (長さ-1) 演算は長さを剰余する、つまり h%length を取得するのと同等ですが、& は % より効率的です。
配列の長さが 2 の n 乗である場合、異なるキーによって同じインデックスが計算される確率は小さいため、データは配列上に均等に分散され、衝突の確率が小さいことを意味します。対照的に、クエリの場合は、リンクされたリストの特定の位置をたどる必要がないため、クエリの効率が高くなります。

上記の put メソッドのソース コードによると、プログラムがキーと値のペアを HashMap に配置しようとすると、プログラムはまずキーの hashCode() 戻り値に基づいてエントリの保存場所を決定します。 2 つのエントリの hashCode() の戻り値のキーが同じであれば、それらの格納場所は同じです。これら 2 つのエントリのキーが等価比較によって true を返した場合、新しく追加されたエントリの値はコレクション内の元のエントリの値を上書きしますが、キーは上書きされません。これら 2 つのエントリのキーが等価比較によって false を返した場合、新しく追加されたエントリはコレクション内の元のエントリとエントリ チェーンを形成し、新しく追加されたエントリはエントリ チェーンの先頭に配置されます - 引き続き説明を参照してください。特定の命令の addEntry() メソッドの。
(2)

を読む
public V get(Object key) {  
    if (key == null)  
        return getForNullKey();  
    int hash = hash(key.hashCode());  
    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.equals(k)))  
            return e.value;  
    }  
    return null;  
}
ログイン後にコピー

有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

3) 归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
4. HashMap的resize(rehash):
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
5. HashMap的性能参数:
HashMap 包含如下几个构造器:
HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。
HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。
initialCapacity:HashMap的最大容量,即为底层数组的长度。
loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。
负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。
HashMap的实现中,通过threshold字段来判断HashMap的最大容量:

threshold = (int)(capacity * loadFactor);
ログイン後にコピー

结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:

if (size++ >= threshold)     
    resize(2 * table.length);
ログイン後にコピー

6. Fail-Fast机制:
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。

HashIterator() {  
    expectedModCount = modCount;  
    if (size > 0) { // advance to first entry  
    Entry[] t = table;  
    while (index < t.length && (next = t[index++]) == null)  
        ;  
    }  
}
ログイン後にコピー

在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:
注意到modCount声明为volatile,保证线程之间修改的可见性。

final Entry<K,V> nextEntry() {     
    if (modCount != expectedModCount)     
        throw new ConcurrentModificationException();
ログイン後にコピー

在HashMap的API中指出:

すべての HashMap クラスの「コレクション ビュー メソッド」によって返されるイテレータはフェイルファストです。イテレータの作成後、マップの構造が変更された場合、イテレータ自体の Remove メソッドを使用した場合を除き、それ以外の場合はメソッドの変更が行われます。イテレータは ConcurrentModificationException をスローします。したがって、同時変更に直面した場合、反復子は、将来の不確定な時点で任意の非決定的な動作を危険にさらすことなく、すぐに完全に失敗します。

イテレーターのフェイルファスト動作は保証されていないことに注意してください。また、一般に、同期されていない同時変更がある場合には、確実な保証を行うことは不可能です。フェイルファスト反復子は、ベストエフォートベースで ConcurrentModificationException をスローします。したがって、この例外に依存するプログラムを作成するのは間違いであり、イテレータのフェイルファスト動作はプログラム エラーを検出するためだけに使用するのが正しいアプローチです。

関連する推奨事項:

Java の HashMap の実装原理を深く理解する (写真)

Java ロックフリー ハッシュマップの原理と実装を詳細に説明する

以上がJavaにおけるHashMapの実装原理の解析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

ハッシュマップの展開メカニズムは何ですか? ハッシュマップの展開メカニズムは何ですか? Mar 15, 2023 pm 03:39 PM

ハッシュマップの拡張メカニズムは、容量を再計算し、元の配列を新しい配列に置き換えることです。元の配列のすべてのデータを再計算し、新しい配列を挿入し、新しい配列をポイントします。配列が容量拡張前の最大値に達している場合は、しきい値を最大の整数に直接設定して返します。

HashMap クラスの put() メソッドを使用して HashMap にキーと値のペアを挿入する方法 HashMap クラスの put() メソッドを使用して HashMap にキーと値のペアを挿入する方法 Jul 26, 2023 pm 11:53 PM

HashMap クラスの put() メソッドを使用して、キーと値のペアを HashMap に挿入する方法。HashMap は、Java コレクション フレームワークの非常に重要なクラスです。キーと値のペアを格納する方法を提供します。実際の開発では、多くの場合、キーと値のペアを HashMap に挿入する必要があります。これは、HashMap クラスの put() メソッドを使用することで簡単に実現できます。 HashMap の put() メソッドのシグネチャは次のとおりです: Vput(Kkey,Vvalue)

Java HashMap に基づいて、重複した Key 値を挿入する問題を解決する方法 Java HashMap に基づいて、重複した Key 値を挿入する問題を解決する方法 May 09, 2023 am 10:52 AM

javaHashMap に重複する Key 値を挿入する HashMap に重複する値を挿入するには、まず要素が HashMap にどのように格納されるかを理解する必要があります。 put メソッド Map に格納される各要素はキーと値のペアであり、それらはすべて put メソッドを通じて追加され、Map 内で同じキーに関連付けられる値は 1 つだけになります。 Mapではputメソッドを以下のように定義しています。 Vput(Kkey,Vvalue); put() メソッドの実装: 最初に hash(key) でキーの hashcode() を取得し、取得したハッシュコードに基づいて挿入される位置のチェーンを hashmap で見つけます。

Javaドキュメントの解釈: HashMapクラスのcontainsKey()メソッドの使用法の詳細な説明 Javaドキュメントの解釈: HashMapクラスのcontainsKey()メソッドの使用法の詳細な説明 Nov 04, 2023 am 08:12 AM

Java ドキュメントの解釈: HashMap クラスの containsKey() メソッドの使用法の詳細な説明 特定のコード例が必要です はじめに: HashMap は Java で一般的に使用されるデータ構造であり、効率的なストレージおよび検索機能を提供します。 containsKey() メソッドは、HashMap に指定されたキーが含まれているかどうかを判断するために使用されます。この記事では、HashMap クラスの containsKey() メソッドの使用方法を詳しく説明し、具体的なコード例を示します。 1.続き

JavaのLinkedHashMapとHashMapの違いは何ですか JavaのLinkedHashMapとHashMapの違いは何ですか May 02, 2023 am 08:31 AM

1. Map は基本的に HashMap を使用できますが、HashMap には問題があります。つまり、HashMap の反復順序が HashMap の配置順序と異なっているか、順序が狂っていることを説明します。 HashMap のこの欠点は、多くの場合問題を引き起こします。これは、シナリオによっては、順序付けされた Map (LinkedHashMap) が期待されるためです。 2. 相違点インスタンス publicstaticvoidmain(String[]args){Mapmap=newLinkedHashMap();map.put("apple","Apple");map.put(&quot

Java は、HashMap クラスの putAll() 関数を使用して、Map を別の Map に追加します。 Java は、HashMap クラスの putAll() 関数を使用して、Map を別の Map に追加します。 Jul 24, 2023 am 09:36 AM

Java は、HashMap クラスの putAll() 関数を使用して、Map を別の Map に追加します。Map は Java で一般的に使用されるデータ構造であり、キーと値のペアのコレクションを表すために使用されます。 Java のコレクション フレームワークでは、HashMap が一般的に使用される実装クラスです。これは putAll() 関数を提供します。この関数は、あるマップを別のマップに追加して、データのマージとコピーを容易にするために使用されます。この記事では、putAll() 関数の使用方法と、対応するコード例を紹介します。初め、

Java Map のパフォーマンス最適化が明らかに: データ操作をより高速かつ効率的に Java Map のパフォーマンス最適化が明らかに: データ操作をより高速かつ効率的に Feb 20, 2024 am 08:31 AM

JavaMap は、Java 標準ライブラリで一般的に使用されるデータ構造であり、キーと値のペアの形式でデータを格納します。 Map のパフォーマンスは、アプリケーションの実行効率にとって非常に重要です。Map のパフォーマンスが低いと、アプリケーションの実行が遅くなったり、クラッシュしたりする可能性があります。 1. 適切な Map 実装を選択します。Java には、HashMap、TreeMap、LinkedHashMap などのさまざまな Map 実装が用意されています。各 Map 実装には独自の長所と短所があるため、Map 実装を選択するときは、アプリケーションの特定のニーズに基づいて適切な実装を選択する必要があります。 HashMap: HashMap は最も一般的に使用される Map 実装であり、ハッシュ テーブルを使用してデータを保存し、挿入、削除、検索の速度が速くなります。

HashMap を使用して Java シングルトン モードでデータをキャッシュする方法 HashMap を使用して Java シングルトン モードでデータをキャッシュする方法 May 13, 2023 am 09:43 AM

1. シングルトン パターンとは何ですか?シングルトン パターンは、オブジェクトの特定のインスタンスを生成するために使用されるオブジェクト作成パターンであり、システム内のクラスのインスタンスが 1 つだけ生成されるようにすることができます。 Java で実装されたシングルトンは仮想マシンのスコープ内にあり、クラスをロードする機能は仮想マシンに属するため、仮想マシンは独自の ClassLoad を通じてシングルトン クラスをロードするときにクラスのインスタンスを作成します。 Java 言語では、このような動作により 2 つの大きな利点がもたらされます: 1. 頻繁に使用されるオブジェクトの場合、オブジェクトの作成にかかる時間を省略できます。これは、これらの重量のあるオブジェクトにとって非常に大きなシステム オーバーヘッドになります; 2. 新しい操作の数が増えるため、が減少すると、システム メモリの使用頻度も減少し、GC 圧力が軽減されます。

See all articles