Javaの知識が4を追加する:HashMapのは、レビューの詳細を

ストレージのHashMapの

一般に、ハッシュ・テーブルの概念は、アレイの形態で記憶されているプラ​​スHashMapのリストをリンクされ、データ構造は、実質的に同じです。

Javaのハッシュマップでは、キーと値のペアの各キーは、一組のエントリとしてみなされます。

Javaは、エントリキーのハッシュ値を計算します。算出されたハッシュ値、ハッシュ値の異なるキーと同様に、ハッシュ衝突は、現時点(ハッシュ衝突)で生成された場合、ハッシュテーブルに計算されたハッシュバケットは、同じハッシュバケットに、格納されます両方のエントリは、リンクされたリストに保存されています。異なるストレージJDK実施形態の異なるバージョンでは、ヘッドは、近端から配列に新しい要素を挿入するために、即ち、JDK7補間方法です。jdk8新しい要素がアレイから離れた一端に挿入されます。

一般に次のように示され:

複雑さとHashMapのインサートを検索し、削除概念的な複雑さはO(1)であり、記憶するのは非常に効率的な方法であると言うことができます。

HashMapのインスタンス化

HashMapのソースの例:
コンストラクタは空です。

public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
        //new HashMap();未指定容量时,初始容量16,DEFAULT_LOAD_FACTOR为0.75
    }

空の引数のコンストラクタは、コンストラクタの2つのパラメータを呼び出します:
どのくらい我々まずこれらの2つの参照を見て:

static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;

図から分かるように、パラメータが指定されていない場合、16のデフォルトサイズは、ハッシュマップ0.75の負荷率を生成します。
ここでは、このコンストラクタを見て2つのパラメータがあります。

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)//初始容量小于零,报错
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)//初始容量大于了最大容量常量1073741824
            initialCapacity = MAXIMUM_CAPACITY;//将容量设为最大容量常量1073741824
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;  //这里创建的容量永远为2的整数次方
                            //比如new HashMap(15) 实际上new了大小为16的HashMap

        this.loadFactor = loadFactor;//0.75---加载因子,下面让16乘加载因子等于临界值(12)
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];//即填超过12个数的时候就开始扩容,而不是到16以上才扩容
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

手動でこれらの2つのパラメータを満たしている場合:

  • HashMapのの大きさのために、コードのこの部分は重要です。
int capacity = 1;
while (capacity < initialCapacity)
    capacity <<= 1;  //这里创建的容量永远为2的整数次方
//比如new HashMap(15) 实际上new了大小为16的HashMap

業務効率の基盤となるJavaのシフトが直接乗算よりも高くなっているので、ここでの左には、実際に第二を取り、なぜ左にある必要があります。
容量サイズが1に設定され、その結果がいっぱいパラメータより大きいまで常に、2によって残されます。
図から分かるように、基本となるハッシュマップの初期の実容量は:
パラメータにはフィル、16の容量がない場合1.
パラメータのサイズに充填2.、容量が2の最小に充填された整数パラメータの力よりも大きいです。
3.hashmap展開は常に二回二回に拡大しています。

  • 負荷率の場合:
 this.loadFactor = loadFactor;//0.75---加载因子,下面让16乘加载因子等于临界值(12)
 threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
 table = new Entry[capacity];//即填超过12个数的时候就开始扩容,而不是到16以上才扩容

負荷率を意味することになる要素のハッシュマップの数を超えた場合に容量*負荷率の時間を、ハッシュマップ自動拡張ではなく、ハッシュマップ完全な再膨張後などであろう。

なぜ負荷率が0.75でありますか?

JDKソース設計負荷率が0.75で0.75のハッシュマップの動的拡張である場合、なぜなら時間効率および空間効率上記バランス負荷率が高いほど、スペースの効率が占領しますが、リストをもたらす可能性が低いクエリ効率が得られ、長すぎます。負荷率が低すぎる、スペースの無駄の多くのがあるでしょう。以下のために:0.75であるため与えられた理由は、ポアソン分布、同じハッシュバケットがより8つの確率は非常に小さいよりも、無視することができました。したがって、確率リストとして0.75の負荷率は、8つの要素が非常に小さく表示されることができます。

put操作のHashMapの(JDK7)

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);//可以放null的key
        int hash = hash(key);//计算key的哈希值
        int i = indexFor(hash, table.length);//return hash & (table.length-1)
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍历链表的循环
            Object k;
            //如果哈希值相等,且key相等,用新定义的entry替换原有的entry
            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);//没有进去的情况下,直接在空位置放进去k,v即可
        return null;
    }

我々は、これら2つの文でキー嘘、計算されたエントリーは、位置に配置する方法を確認するために、ソースコードに焦点を当てます:

int hash = hash(key);//计算key的哈希值
int i = indexFor(hash, table.length);//return hash & (table.length-1)

私たちは、indexFor静的メソッドを探し続けて:

static int indexFor(int h, int length) {
        return h & (length-1);
    }

H&(長さ1)
エントリインデックスの配置は方法によって計算されます。

なぜ、HashMapの容量は常に2の累乗でありますか?

記憶されたエントリの位置ので、この問題は今、通って説明することができるH&(長さ1)計算は、配列の長さを仮定すると、常に2のn乗であり、我々はそれを知ることができ、:2 ^ n個このすべてShenerの下のバイナリ長さ

1000000000 ...
すべてのビットが0になった後、彼らは常に1つの最初の場所です、そしてこの数から1を引いた場合、あなたは(長さ- 1)を得ることができるおおよそのこのShenerです。

011111111 ...
彼らは最初に0ですが、すべてのビットの後ろにあります

:1はそれの結果として、私たちはインデックスの計算式があることを忘れないようにしましょう、位置に対応する二つの数字です:私たちは、ビット単位演算であることを知っている
H&(長さ-1)
我々は時間の指数を計算した場合このバイナリ長-1デジタル上記次いで、これらの位置、0に多くのビットを有する決して決して1で得られた結果!これになります計算された格納位置一致が高すぎる、ハッシュバケットがあったているが空で、スペースの無駄ではないだけでなく、おそらくリストが長すぎる、また効率の廃棄物であるスペースの廃棄物として記述することができますされて引き起こします。HashMapの2のn乗の長さは、効率と空間の間の完璧なバランスである、と言うことができます

ハッシュ値は来る方法ですか?

JDK7

final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {//默认false,进不来
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();//得到Object k的hashCode再亦或

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12); //再进行一系列无符号右移加亦或
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

JDK7は、ハッシュ値を計算する方法である、または第一の符号なし右シフトまたは取得され、最終的なハッシュ値のシリーズを通して、次にハッシュコードを取得します。
なぜ、計算されたハッシュ値は、非常に多くの操作を必要としますか?
かどうかはJDK7 / 8、しばしば非常に長いのhashCodeが、長さはそう簡単に、唯一の情報の一部を取得することが低ければ、それは常に右にシフトする必要があるので、ハッシュ衝突を起こしやすい、計算したハッシュ値ならばつながる、非常に短いことが多いです従って、衝突の確率を減少させる、来る情報の高水準。

jdk8

大幅に簡略化されたjdk8ハッシュ値を算出する方法、

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

最初はハッシュコードを取得することであり、**(H = key.hashCode()) ^(H >>> 16)**
すなわちハッシュコード16および16ビット演算を下げるか高くなります。だから我々は高いのhashCode補足情報の効果を達成することができます。

addEntry()メソッドメソッドの実装

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {//长度大于12且要放的位置不为空时,进行下面操作
            resize(2 * table.length);//扩容为原来的二倍
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);//不需要扩容
    }

図から分かるように、要素を追加した後の要素数が閾値(容量* load_factory)、それは自動的に拡張を超えた場合。拡張後のハッシュ値を再計算し、新しいハッシュ値に基づいてインデックスを計算します。
ここで、拡張メソッドにリサイズ()に指します。

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        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;
            }
        }
    }
                e.next = newTable[i];
                newTable[i] = e;
                e = next;

これは、操作方法を補間拡張HashMapの後のコード要素の最初の3行を完了しました。図以下:
ここに画像を挿入説明
jdk8は、もはや第1の補間方法で使用されません。

デッドロックのHashMapの

分析のデッドロックは、この記事を見ることができました

https://coolshell.cn/articles/9606.html

結論として、に来て、膨張を完了T2スレッド膨張、展開時t1までのT1、T2のスレッド、マルチスレッド環境です。展開が完了するとt1は、追加の要素へのポインタであり続けて2つのスレッドのテールリストの良好な拡張を持って、再びこのよう拡張した後、円形のリンクリスト、クエリ、デッドロックを形成することになります。
拡張ヘッド補間点でJDK7デッドロックを発生する膨張要素が変わらないの前と後に、一次補間法は保証されません。
そしてjdk8は、デッドロックを減少させる、要素の順序を維持するために、尾部要素を使用して、前と伸張後の補間を加えました。

而且jdk8中,元素在扩容后新的哈希桶的位置总是在原来的位置或者:(原来的位置+原来map的容量) 处的哈希桶内。
我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。 看下图可以明白这句话的意思,n 为 table 的长度,图(a)表示扩容前的 key1 和 key2 两种 key 确定索引位置的示例,图(b)表示扩容后 key1 和 key2 两种 key 确定索引位置的示例, 其中 hash1 是 key1 对应的哈希与高位运算结果。
ここに画像を挿入説明
元素在重新计算 hash 之后,因为 n 变为 2 倍,那么 n-1 的 mask 范围在高位多 1bit(红色),因 此新的 index 就会发生这样的变化:
ここに画像を挿入説明
及扩容前假如只取了4位,扩容后直接看最高位的数字是1还是0即可,0的话还是以前的位置,1的话向高位走一个旧容量的距离。

jdk8 HashMap的优化

1.以上提到的计算hash值的方法。
2.为了避免死锁放弃头插法,并且扩容时保持链表顺序,且大大简化了重新哈希的过程,只看最高位是1还是0即可确定新的位置。
3.链表长度超过8的时候转化为红黑树存储,提高效率,为什么设计成8个?因为哈希桶内的元素个数符合e=0.5的泊松分布,n>8时,概率约为十万分之一非常小。当红黑树元素少于6个的时候,再次退化为链表。

发布了16 篇原创文章 · 获赞 2 · 访问量 425

おすすめ

転載: blog.csdn.net/qq_31314141/article/details/104383301