HashMap のソースコード分析とインタビューの質問

目次

5. HashMap ソースコード分析

5.1. 容量の初期化

5.2 負荷率とは?

5.3. 負荷率は 0.75 より大きくても小さくてもかまいませんか?

5.4. 拡張の長さは?

5.5. 下付き文字はどのように計算されますか? 

5.6. ハッシュ競合を解決するには?

5.7. いつツリーに変換されますか?

5.8、HashMap データ構造


5. HashMap ソースコード分析

5.1. 容量の初期化

put メソッドを実行する前は容量が 0 で、put メソッドを実行すると、デフォルトの初期容量として 16 が割り当てられます。

ソース コードでは、パラメーターを構築せずに HashMap を初期化すると、0.75 の負荷係数が割り当てられます。ただし、容量は割り当てられておらず、初期化されていません。

この時点で初期容量は 0 です。

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
public class HashMapDemo {
    public static void main(String[] args) {
        HashMap hashMap=new HashMap();
        //调用put方法
        hashMap.put("a","1");
    }
}

 ソースコードを入れる

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

このとき、キーのハッシュ値、キーの値、および値の値を渡す putVal メソッドが返されます。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //tab是一个数组,初始化为null,走第一个if语句
        if ((tab = table) == null || (n = tab.length) == 0)
            //这里调用了一个resize()方法,也就是扩容方法
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize() 展開方法 

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //HashMap初始化时oldTab为null,经过三目运算为0,那么不会走下面的if语句
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //最终会走这里的赋值,将newCap容量赋值为默认的容量16
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

 上記のソース コードからわかるように、HashMap の初期容量は、put メソッドを呼び出す前は 0 であり、put メソッドを使用した後は 16 です。

5.2 負荷率とは?

0.75、拡張しきい値とも呼ばれます

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

5.3. 負荷率は 0.75 より大きくても小さくてもかまいませんか?

いいえ、拡張時に容量は負荷率*によって初期化されるため、デフォルトは 16*0.75=12 です。つまり、容量が 12 を超える場合、元の容量が 2 倍に拡張されます。

しかし、負荷率が 0.5 など 0.75 未満の場合、16*0.5=8 の場合、容量拡張が必要になり、リソースの浪費につながります。

16*1=16 のように、負荷率が 0.75 より大きい場合、クエリと挿入の数が増加し、パフォーマンスが低下します。

5.4. 拡張の長さは?

各展開は元のサイズの 2 倍です

        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE; //2的30次方
                return oldTab;
            }
            //如果没有超过最大值,则扩大容量为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 2的一次方 2倍
        }

5.5. 下付き文字はどのように計算されますか? 

(配列の長さ - 1) キーのハッシュ値でビット操作を実行します

i = (n - 1) & ハッシュ

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;
        //计算HashMap的下标
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

5.6. ハッシュ競合を解決するには?

ハッシュ競合とは、HashMap の基になる配列で、新しく追加されたデータで、以前に存在したデータと同じハッシュ値を持っているが、キーが異なることが判明したことを意味します。配列内のメモリ アドレスは 1 つのデータしか格納できないため、複数のデータを格納するにはどうすればよいでしょうか? 2 つのキーのハッシュ値が同じで、2 つのキーの equals が false の場合は、連結リストまたは赤黒木を使用します。


5.7. いつツリーに変換されますか?

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

p.next はインデックス 1 から 7 までトラバースし、binCount>=TREEIFY_THRESHOLD-1 の条件を満たし、ツリーに変換しようとします。インデックスが 7、つまり長さが連結リストは8なので、連結リストの長さが8以上の場合はまだ終わっていません、ツリーに変換しようとしているから、treeifBinメソッドを確認してください

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //如果数组为null或者数组长度小于64,则进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //否则,就会转换为树
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
 static final int MIN_TREEIFY_CAPACITY = 64;

ソース コードでは、配列の長さが 64 以上の場合、次の変換番号コードが実行されることがわかります。

概要: リンク リストの長さが 8 以上で、配列の長さが 64 以上の場合、リンク リストはツリーに変換されます。

5.8、HashMap データ構造

jdk1.8 より前の hashMap の基になるデータ構造は、配列 + リンク リストです。

jdk1.8以降のhashMapのデータ構造は、配列+連結リスト+赤黒木

おすすめ

転載: blog.csdn.net/select_myname/article/details/128293923