JavaのHashMapの作品

プロセスは以下のものを使用してHashMapのとほぼ同様です。

HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("语文", 1);
map.put("数学", 2);
map.put("英语", 3);
map.put("历史", 4);
map.put("政治", 5);
map.put("地理", 6);
map.put("生物", 7);
map.put("化学", 8);
for(Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

何が起こったのか?ここでは、HashMapのの構造の感情的な理解を持っている一般的な構造を願っている:
ここに画像を挿入説明
公式ドキュメントは、HashMapの中で説明されています。

ハッシュテーブルMapインタフェースの実装に基づきます。この実装は、オプションのマップオペレーションをすべて提供し、null値およびnullのキーを許可します。(HashMapのクラスは、それが非同期であり、ヌルを許容することを除いて、ハッシュテーブルとほぼ同等である。)このクラスはマップの順序について何ら保証しません。特に、それは順序を一定に保つことを保証するものではありません。

2つの重要なパラメータ

HashMapの中に2つの非常に重要なパラメータ、容量(キャパシティ)および負荷率(ロードファクター)があります。

  • 初期容量は、容量がハッシュテーブル内のバケットの数は、初期容量は単純にハッシュテーブルが作成された時点での容量です。
  • 負荷率負荷率は、ハッシュテーブルは、その容量が自動的に増加する前に取得することが許可されているか、フルの尺度です。

簡単に言えば、容量はバケット数で、負荷率は、充填バケットの最大程度の割合です。反復高いパフォーマンス要件場合は、入れていない容量の設定が高すぎる、それはあまりにも低負荷率を設定すべきではありません。場合バケット数(ハッシュマップの要素、すなわち数)の容量を充填するの数は、*負荷率が倍現在のバケットのために調整する必要性よりも大きいです。

プット機能

一般的なアイデアプット機能:

  1. hashCodeのキー()ハッシュを行い、その後、インデックスが計算されます。
  2. あなたは、バケット内部に直接クラッシュしない場合は、
  3. 衝突の場合、リンクリストの形でバケットの存在;
  4. 衝突が長鎖(より大きいか等しいTREEIFY_THRESHOLD)をもたらす場合、赤黒木プットリスト(1.8)に変換します。
  5. ノードがすでに古い値を置き換えるに存在する場合(キーの一意性を保証)
  6. バケツがいっぱいになった場合(負荷率よりも*電流容量=しきい値)、それはサイズを変更しなければなりません。
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 如果key已经存在,则替换value,并返回旧值
    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++;
    // key不存在,则插入新的元素
    addEntry(hash, key, value, i);
    return null;
}
ハッシュ関数
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

16ビット定数の高い、低いおよび高い16ビット、16ビットのXORを作った:私たちは、これがおそらく役割の関数で見ることができます。コードのコメントはこのように書かれている場合:

Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.

長さの現在のテーブルとして、ハッシュ関数を設計N 2のパワーであり、そして次の目標時間を計算するとき(オペレーション代わり%の残りの部分を使用して&ビット)が達成されます。

(n - 1) & hash

設計者は、この方法がクラッシュしやすいと考えています。なぜあなたは言うのですか?nは考えることを望むかもしれない - 1は15(0x1111)で、実際には、ハッシュが有効に低い4ビットの唯一の重要なビットを取り、もちろん、簡単に墜落しました。

そのため、設計者は全体の状況(速度を考慮し、機能、品質)の道を望んでいた、高い16ビットおよび16ビットXORビット低いです。設計者はまた、ハッシュコードの分布のほとんどは、それはまた、O(LOGN)それを行うには木に衝突した場合であっても、非常に良いされているためと説明しました。だけでなく、システムのオーバーヘッドを削減それとも違って見える、それは(テーブル長が小さい)下の根本的な原因は、計算されているため、衝突を引き起こし、ハイレベルに参加しなかったことはありません。

それでも頻繁に衝突を持っていた場合、何が起こるのだろうか?著者のノートは、彼らがJEP-180、問題の説明では、(我々はビンでの衝突の大規模なセットを処理するために木を使用)頻繁に衝突を処理するために木を使用することを言いました:

マップエントリを保存するためにバランスの取れた木ではなく、リンクリストを使用することにより、高ハッシュ衝突条件の下でのjava.util.HashMapの性能を向上しました。LinkedHashMapクラスで同じ改善を実装します。

前述のように、HashMapの要素を取得するに、基本的な2段階のプロセス:

  1. 最初のhashCode()ハッシュ、その後、決定したインデックスのバケツを行います。
  2. バケツの主要ノードは、我々が必要とするものでない場合は、()keys.equalsでチェーン内で検索します。

リンクされたリスト内の8のJava競合解決前に達成され、衝突の場合には、取得したときに、二段階の時間複雑度はO(1)+ O(N)です。衝突は非常に強力であるとき、nは大きいときしたがって、O(n)は、明らかにスピードの速さに影響を与えます。

従ってジャワ8、赤黒木の置換リストを使用するので、複雑になることがO(1)+ O(LOGN)などときに大きなNで、この問題に対する理想的なソリューションであることができます。

リサイズ機能、転送機能

置いたとき、あれば我々はそれが起こるサイズが変更されます、占有の現在のバケットレベルは負荷率所望の割合を超えている見つけます。サイズ変更の過程で、単にバケットが2倍に拡大した後、インデックスを再計算し、ノードと、新しいバケツに入れていることを意味します。リサイズノートには、それをこのように説明しました:

初期化したり、テーブルのサイズを倍になります。nullの場合は、初期容量の目標にあった割り当ては、フィールドしきい値で開催されました。我々は2のべき乗の拡張を使用しているので、そうでない場合は、各ビンの要素は、いずれかの新しいテーブル内のオフセット2の電源と同じインデックス、または移動に滞在しなければなりません。

おおよその限界を超えたときにはサイズが変更されます、ということを意味するだけでなく、原位置において、いずれかの元の場所にいずれかの要素の位置ので、我々は、(元の2倍の拡張の長さを参照)を2回拡張電力を使用しているためモバイルパワーポジション2。

したがって、我々は、HashMapのが唯一の1つのまたは0ビットのようである新しいオリジナルのハッシュ値を調べる必要があり、ハッシュを再計算する必要はありません拡大」、インデックスは、インデックスが1になる、変更されていない、0であります元のインデックス+ oldCap」。

この設計では、ハッシュ値を再計算するための時間の必要性を排除すると同時に、新たに1ビットのためには、競合がノードを分散化する前であっても、プロセスのサイズを変更し、ランダム0または1とみなすことができるだけでなく、確かに非常に賢いです新しいバケツへ。

チェック容量がしきい値しきい値に達しています。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

達成するための拡張:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ...

    Entry[] newTable = new Entry[newCapacity];
    ...
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

ここでは、より大きな配列を作成し、転写法により、要素を移動します。

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
マルチスレッド安全性の問題

これらは、ロジック関連するモバイルノードです。

ここに画像を挿入説明
第4のノードを挿入するとき、焼き直しが発生した場合、我々は2つのスレッドが同時に存在すると仮定し、スレッド1とスレッド2は、2つのスレッドが新しい新しい配列です。

ここに画像を挿入説明
2 <K、V>次= e.nextエントリの実装で想定されているスレッド、以降、CPUタイムスライスが満了し、時刻e変数は、次の変数ノードBに、点ノード。

スレッド1は、焼き直しがモバイルノードを起動するために同じ場所7にした後、それは、残念ながら、B、Cのノードで、継続します。

最初のステップは、モバイルノードA。
ここに画像を挿入説明
第二ステップ、モバイルノードb。
ここに画像を挿入説明
順序が逆になることに留意されたいは、モバイルノードは、Cを継続します。
ここに画像を挿入説明
このタイムスライスの時間スレッドを1つのランアウトは、テーブルの内部が新たNEWTABLEに設定されていない、スレッド2の実行を開始し、次のように、内部時間基準関係は、
ここに画像を挿入説明
この場合、スレッド2は、変数e点は、次の変数ノードNode Bに点が、残りのロジックは、ループ本体を開始します。

Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;

実行後、次の図の参照関係。
ここに画像を挿入説明
eがnullではないので、点eまで実行、変数ノードBの後、次いで、ループは、参照関係を実行した後、続けています。
ここに画像を挿入説明
eがnullではないので、点eまで実行、変数ノードBの後、次いで、ループは、参照関係を実行した後、続けています。
ここに画像を挿入説明
変数eは再び、唯一のループ、慎重な分析の下を続けることができますバックノードaへのポイント:

  1. エントリ<K、V>次= e.nextを行った後、現在のノードが変数ヌル隣、次指していません。
  2. e.next = NEWTABLE [i]は、前記NEWTABLE [I]は、ノードBに次の点であるノードBに点、及びBそれぞれ他のそのような参考文献に、環を形成します。
  3. NEWTABLE [I] =位置にノードのアレイのE I。
  4. E =次、変数eを割り当てるヌル変数がNULL次のポインタである第一段階として、です。

だから、最後に、これは関係への参照です。
ここに画像を挿入説明

概要

拡張が発生したときに、同時の場合には、それは実装に入る、循環リストを生成することができ、それは100%のCPUの問題を引き起こし、無限ループがトリガされますので、同時実行環境でHashMapを使用しないようにしてください。

HashMapの補間は解析されたバージョンのテールJDK1.8を実装しました

HashMapのJDK1.8は、配列+ +赤黒木の形のリストにバージョン、全体のデータ構造を最適化した後。そして、尾は補間が名前付きリストの尾、尾の補間のためのリスト要素に、この場合には、リストに浴槽か赤黒木上記のHashMap素子アレイ位置の内側に新しい位置を配置するときに言いました。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
            boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 数组是否未初始化?若未初始化则进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // key的hash值经过位运算之后再和数组长度-1得到的值运算得到key在数组的下标
    // 若数组的这个位置还没有元素则直接将key-value放进去
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 若该下标位置已有元素
        Node<K,V> e; K k;
        // 是否已有元素的key值与新增元素的key判断是同一个
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 直接覆盖value
            e = p;
        // 如果已有元素是树节点
        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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        /**
        * 删除部分代码 
        */
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

テールJDK1.8にHashMapの補間方法が実装されている特定の方法を、実装次ルックp.next = newNode(ハッシュ、キー、値、NULL):

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

Node(int hash, K key, V value, Node<K,V> next) {
    this.hash = hash;
    this.key = key;
    this.value = value;
    this.next = next;
}

この方法の内側に、ノードは(エントリ<K、V>から継承された新しい実装JDK1.8、)新しいノードを作成し、その後継ノードは、要素のリスト内の後のヌル点位置を記載されていません。

後続ノードp.next =元のトラバースの最後の要素この文は、新しいノードのノード構成に点であるnewNode(ハッシュ、キー、値、ヌル)を見つけるためにリスト内。正味の効果は、実際にはJDK1.8テール補間でのHashMapで、リストの末尾に追加された新しい要素です。

公開された273元の記事 ウォン称賛13 ビュー70000 +

おすすめ

転載: blog.csdn.net/LU_ZHAO/article/details/105137220