15、HashMapの作品と膨張機構

1. HashMapの作品

多くのプログラミングシナリオが私たちのために働くにはHashMapのように良いのJavaコレクションフレームワークは、重要なメンバーです。その作品の面で達成するハッシュテーブルのデータ構造としてHashMapのは、ブログの条項が、ポイントに個別に記載されています。HashMapのの原則の実現のためにのみ簡単な概要を行うので、本稿では、その膨張機構の概要ですので。

HashMapのバケットアレイ、単一リンクされたリストの最初のノードに格納された各バケット内部に実装されています。前記各ノードは、キー値ペアは(エントリ)一体である格納ファスナーを使用してHashMapの解決ハッシュ衝突法(ハッシュ衝突に後で導入されます)。

HashMapの最適化Java8所々ので、次の要約およびソースコード分析は、Java7に基づいています。

次のように図です。


3290394-bea1bb72616a6599.png
20171119123859600.png

HashMapのは(K、V)を入れ、二つの重要な基本的な操作を提供し、(K)を取得します。

  • プット操作が呼び出されたとき、HashMapのキーK計算されたハッシュ値は、ハッシュマップに1つのバケット(バケット)にマッピングされ、この場合、第1ノード単鎖用浴槽を発見し、順次トラバースエントリキーでノードを検索するための単一のリストが与えられたパラメータKに等しく;見つかった場合、それは古いV V指定されたパラメータに置き換えられ、エントリは、そうでない場合は、新しいノードを挿入し、直接リストテールをリンクされています。

一般的な考え方入れ機能:

図1に示すように、キーのhashCode()のインデックスを計算し、次に、ハッシュを実行する;
2、そうでない場合は、直接衝突におけるバケットに;
3、衝突、バケットの形でリンクされたリスト場合;
4、リストが長すぎる衝突が生じる場合には( ;以上)TREEIFY_THRESHOLD、より等しいか5は、赤黒木リストプットに変換
古い値(キー保証一意)に代わるノードがすでに存在する場合、5
バケットが負荷率*電流容量)よりも(一杯になった場合、6れ、サイズを変更。

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值在对应桶中的索引。
    int i = indexFor(hash, table.length);
    // 如果 i 索引处的 Entry 不为 null,通过遍历桶外挂的单链表
    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++;
    //遍历单链表完毕,没有找到与键相对的Entry,需要新建一个Entry放在单链表中,
    //如果该桶不为空且数量达到筏值,有可能触发扩容
    addEntry(hash, key, value, i);
    return null;
}
  • GET、PUTオペレーションの動作と同様(K)、ハッシュ値計算によるハッシュマップのキーは、対応するバケットを見つけるために、及び、対応するキーを検索するために値の参照エントリが格納されているタブ単一のリストを横断するため。

大まかなアイデアを得るための機能

図1に示すように、バケット(単一鎖の非存在下)の最初のノード、ダイレクトアクセス、
競合がある場合、対応するエントリを見つけるために、(K)を介して2 key.equals
場合key.equalsスルー木、木見つける(K)、O(LOGN)は、
リンクされたリストと、O(n)は、リスト内のkey.equals(K)によって検索されます。

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;
}

最も重要な理由は、頻繁に保存することがあまりにも多くのエントリのようなハッシュ衝突を生成し、バケットは、これと供給不足で同様の紛争は、十分ではありません。HashMapのがより多くのエントリを保存した場合、したがって、我々はこれ以降の入力が懸念されるため、それは非常にハッシュ衝突を緩和します格納する、バケットの数を増やすことを検討します。

だから、トップの答えなぜ展開とみなさ拡大を、ハッシュマップになる、そうする場合、拡張?どのくらいの拡張?どのように展開?第二部は、要約することです

2. HashMapの展開

2.1 HashMapの拡張機会

プロセスを使用してHashMapのは、私たちはしばしばパラメータを使用して、このようなコンストラクタが発生しました。

public HashMap(int initialCapacity, float loadFactor) ;

最初のパラメータ:初期容量、浴槽の初期指定された数は、バケットアレイのサイズに対応します。
第2のパラメータは:因子をロード、しきい値を決定するために必要とされる能力に基づいて、0と1の間の係数は、であり、デフォルト値は0.75です。
アリババ、このような記述があり、マニュアルを開発します:


3290394-3ee93750f1335d54.png
image.png

上記の機能を説明するために入れたときに単独で対応するエントリをリストトラバーサルをリンクし、それを見つけていない場合は、覚えていますか?この時間は、単一のリスト内のエントリを追加し、addEntry(ハッシュ、キー、値、I)関数と呼ばれます。

 void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
          //当size大于等于某一个阈值thresholdde时候且该桶并不是一个空桶;
          /*这个这样说明比较好理解:因为size 已经大于等于阈 值了,说明Entry数量较多,哈希冲突严重, 
            那么若该Entry对应的桶不是一个空桶,这个Entry的加入必然会把原来的链表拉得更长,因此需要扩容;
            若对应的桶是一个空桶,那么此时没有必要扩容。*/
            //将容量扩容为原来的2倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            //扩容后的,该hash值对应的新的桶位置
            bucketIndex = indexFor(hash, table.length);
        }
        //在指定的桶位置上,创建一个新的Entry
        createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);//链表的头插法插入新建的Entry
       //更新size
        size++;
    }
  • サイズは、エントリマップに含まれるレコードの数です

  • そして、しきい値は記録しきい値としきい値= loadFactorの*容量のサイズを変更する必要があります

  • 容量は、実際にはバレルの長さであります

threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
だから今、私たちは機会の拡大を総括しました:

当map中包含的Entry的数量大于等于threshold = loadFactor * capacity的时候,且新建的Entry刚好落在一个非空的桶上,此刻触发扩容机制,将其容量扩大为2倍。

当size大于等于threshold的时候,并不一定会触发扩容机制(比如增加的entry对应的是一个空桶,那直接加载空桶里面,如果对应的不是空桶,会将链表拉长,就会触发扩容),但是会很可能就触发扩容机制,只要有一个新建的Entry出现哈希冲突,则立刻resize。

3. 总结

我们现在可以回答开始的几个问题,加深对HashMap的理解:

  1. 什么时候会使用HashMap?他有什么特点?
    是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。

  2. 你知道HashMap的工作原理吗?
    HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。它是基于哈希表的 Map 接口的非同步实现。
    他是基于hashing算法的原理,通过put(key,value)和get(key)方法储存和获取值的。

存:我们将键值对K/V 传递给put()方法,它调用K对象的hashCode()方法来计算hashCode从而得到bucket位置,之后储存Entry对象。(HashMap是在bucket中储存 键对象 和 值对象,作为Map.Entry)
取:获取对象时,我们传递 键给get()方法,然后调用K的hashCode()方法从而得到hashCode进而获取到bucket位置,再调用K的equals()方法从而确定键值对,返回值对象。

碰撞:当两个对象的hashcode相同时,它们的bucket位置相同,‘碰撞’就会发生。如何解决,就是利用链表结构进行存储,即HashMap使用LinkedList存储对象。但是当链表长度大于8(默认)时,就会把链表转换为红黑树,在红黑树中执行插入获取操作。

扩容:如果HashMap的大小超过了负载因子定义的容量,就会进行扩容。默认负载因子为0.75。就是说,当一个map填满了75%的bucket时候,将会创建原来HashMap大小的两倍的bucket数组(jdk1.6,但不超过最大容量),来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

3、为什么扩容要以2的倍数扩容?
答案当然是为了性能。在HashMap通过键的哈希值进行定位桶位置的时候,调用了一个indexFor(hash, table.length);方法。

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

&为位与运算符
可以看到这里是将哈希值h与桶数组的length-1(实际上也是map的容量-1)进行了一个与操作得出了对应的桶的位置,h & (length-1)。

但是为什么不采用h % length这种计算方式呢?
因为Java的%、/操作比&慢10倍左右,因此采用&运算会提高性能。

通过限制length是一个2的幂数,h & (length-1)和h % length结果是一致的。这就是为什么要限制容量必须是一个2的幂的原因。

举个简单的例子说明这两个操作的结果一致性:

假设有个hashcode是311,对应的二进制是(1 0011 0111)

length为16,对应的二进制位(1 0000)

%操作:311 = 16*19 + 7;所以结果为7,二进制位(0111);

・操作:(100110111)および(0111)= 0111 = 7ビット(0111)

100110111 =(100110000)+(0111)=(1 2 ^ 4 + 1 2 ^ 5 + 0 2 ^ 6 + 0 2 ^ 7 + 1 2 ^ 8)+ 7 = 2 ^ 4(1 + 2 + 0 + 0 + 16)* 19 + 7 + 7 = 16;及び%の均一な操作。
長さが2の累乗の数である場合には、長さ1がマスクとなり、それは、実際の数がより多いの低いハッシュコードを低いハッシュコードを取り出し、動作性能向上に比べて操作を引き継ぐであろう多くのあろう。
全体的に、法の下付きバレルをindexFor呼び出しビットおよび操作を使用するときに2のべき乗であるlengh規定は、あなたが得ることができ、かつ操作が、私は、コンピューティングの性能の多くを取る位置よりも高くなります。

ます。https://www.jianshu.com/p/c3633291ecdaで再現

おすすめ

転載: blog.csdn.net/weixin_34242658/article/details/91291092