HashMap source code put() method and resize() method that must be asked in the interview -- Based on jdk1 eight

Posted by Ryanz on Wed, 02 Feb 2022 19:40:02 +0100

1, Foreword

Mrs. HashMap is too often used to make too many introductions. Enter the text and directly flush the source code.
This article is mainly based on 1.8 HashMap to tell part of the source code, the main focus is put() method and resize() method

2, Construction method of HashMap

/**
* Set the initialization capacity and call the following overloaded method
*/
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* Setting the initialization capacity and loading factor is only an assignment variable, but it does not initialize the array
*/
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //Sets the length of the array
    this.threshold = tableSizeFor(initialCapacity);
}

/**
* The parameterless construction method only sets a loading threshold, but does not set the initial capacity and expansion threshold. This place should be discussed later when expanding capacity
*/
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

3, put() method

(1) . source code comments

/**
* put Method essentially calls putVal
*/
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 
 /**
 *put method
 */
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //The hashMap constructor does not create an internal array, but only the first put element
    //Determine whether the array is null or the length is 0
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash n is the length of the array, that is, the n-th power of 2. This action is equivalent to finding the remainder
    //If the header node is empty, the element is blocked from the current position
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    //This judgment indicates that the current header node already has elements and hash collision has occurred
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //Judge whether the key value of the current header node is consistent with the key to be placed. If so, replace the old element with the new element
            e = p;
        else if (p instanceof TreeNode)
            //The key value of the current header node is inconsistent with the key to be placed
            //Judge whether the node is a tree node. If it is a tree node, the red black tree stores elements
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
           //If the key value of the current header node is inconsistent with the key to be placed, and it is not a tree node, it indicates that this is a linked list
           //Traversal circular linked list
            for (int binCount = 0; ; ++binCount) {
                //Determine whether the current node is the last node of the linked list
                if ((e = p.next) == null) {
                    // Insert an element at the end
                    p.next = newNode(hash, key, value, null);
                    //Judge whether the length of the linked list is greater than the threshold of transforming red black tree
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //Convert to red black tree. Note that there is another condition in this function: whether the number of buckets in the current hash is greater than 64, otherwise it will not be converted to red black tree
                        treeifyBin(tab, hash);
                    break;
                }
                //The current node is not the last node. Execute logic to judge whether the key value of the current node is consistent with the key to be placed. If so, replace the old element with the new element
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //Replace the current node with the next node
                p = e;
            }
        }
        //Judge whether the e node is null or not, and observe the above code. We will find that there are only two cases of E value. If there is no key to be inserted in the hash, e will point to null; If there is a currently inserted key, e will point to the old node
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //modCount means todo 
    ++modCount;
    //Judge whether capacity expansion is required
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    //If there is a currently inserted key, it has been returned on it, so you only need to return null here
    return null;
}

(2) . flow chart

4, resize() method

(1) . source code comments

final Node<K, V>[] resize() {
      Node<K, V>[] oldTab = table;
       int oldCap = (oldTab == null) ? 0 : oldTab.length;
       int oldThr = threshold;
       int newCap, newThr = 0;
       if (oldCap > 0) {
           // In the first case, there are elements in the array, indicating that it has been initialized
           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
           // In the second case, map < string, string > map = new HashMap < > (16); When new has parameters, it will enter this logic when resizing for the first time
           newCap = oldThr;
       else {               // zero initial threshold signifies using defaults
           /** public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}
             In the third case, when the new map is null and the element is added for the first time, it will enter this loop and set its initial value*/
           newCap = DEFAULT_INITIAL_CAPACITY;
           newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
       }
       if (newThr == 0) {
           //As can be seen from the second case above, the value of newThr is not set
           float ft = (float) newCap * loadFactor;
           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                   (int) ft : Integer.MAX_VALUE);
       }
       threshold = newThr;
       // Create a new empty array
       Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
       table = newTab;
       // When adding elements for the first time, the resize() method will also be executed; If the old array is not empty, it indicates that it is a capacity expansion operation, which involves the transfer operation of elements;
       if (oldTab != null) {
           // Loop the entire array according to the capacity and copy the non empty elements
           for (int j = 0; j < oldCap; ++j) {
               Node<K, V> e;
               // Get the j-th element of the array
               if ((e = oldTab[j]) != null) {
                   // If the current position element is not empty, it needs to be transferred to the new array
                   oldTab[j] = null;
                   if (e.next == null)
                       //There is only one element in the current position, which means that there is no hash conflict in this element
                       newTab[e.hash & (newCap - 1)] = e;
                   else if (e instanceof TreeNode)
                       //The current location node is a tree node
                       ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                   else { // preserve order
                       /** The current location is a linked list
                           Before capacity expansion, the current location is a linked list. After capacity expansion, the elements with hash conflicts may be saved in the original location or moved to the current location + oldCap location,
                           There are two linked lists, so two head nodes and two tail nodes are needed to identify the two linked lists*/
                       Node<K, V> loHead = null, loTail = null;
                       Node<K, V> hiHead = null, hiTail = null;
                       Node<K, V> next;
                       do {
                           next = e.next;
                           // The high order is 0. Note here that e.hash & oldcap. For example, there are two hashes at the beginning, 1001 0011 and 0100 0011 respectively. If the original capacity is 16, 							//  1001 0011 and 0100 0011 are in conflict with the previous 0000 1111 (16-1 = 15) respectively. Now it is 00010000, and the result is different
                           if ((e.hash & oldCap) == 0) {
                               //If there is no tail, it means that the linked list is empty. It is the first time to copy the elements of the new array
                               if (loTail == null)
                                   // When the linked list is empty, the header node points to the element
                                   loHead = e;
                               else
                                   //Add elements at the end and assemble them into a linked list
                                   loTail.next = e;
                               // Set the tail node as the current element
                               loTail = e;
                           }
                           //High order is 1
                           else {
                               if (hiTail == null)
                                   hiHead = e;
                               else
                                   hiTail.next = e;
                               hiTail = e;
                           }
                       } while ((e = next) != null);
                       if (loTail != null) {
                           //When the lower tail node is not empty, the linked list composed of lower elements is still placed in the original position
                           loTail.next = null;
                           newTab[j] = loHead;
                       }
                       if (hiTail != null) {
                           //When the high-order tail node is not empty, the position of the linked list composed of high-order elements is only offset by the length of the old array
                           hiTail.next = null;
                           newTab[j + oldCap] = hiHead;
                       }
                   }
               }
           }
       }
       return newTab;
   }

(2) . flow chart

(3) . precautions for resize method

The resize() method has two caveats

1, This is the assignment of new capacity and new expansion threshold of newcap and newthr. This if else logic judgment corresponds to three cases respectively, which are marked in the above flow chart

(1) The first case: there are already elements in the original map, which belongs to real capacity expansion
(2) The second case: the element is added for the first time, and the HashMap constructed with parameters is used
(3) The third case: the element is added for the first time, and the HashMap constructed without parameters is used

2, When moving the elements in the linked list, you think there may be more elements in the linked list, so you need to move them

For example, if the hashcode of element 3 is 0000 0011 and the hashcode of element 11 is 0001 0011, calculate its position according to the put method
0000 0011 & 0000 1111 = 0000 0011
0001 0011 & 0000 1111 = 0000 0011, then both of them are placed in the position of 3. Note that the relation at this time is (16-1) = 15
The new calculated position is 16
0000 0011 & 0001 0000 = 0000 0000
0001 0011 & 0001 0000 = 0001 0000 judge whether it is placed in the original position or needs to be moved according to whether the calculation result is 0
If movement occurs, two linked lists, one low-level linked list and one high-level linked list will be generated; Therefore, there are four variables: node < K, V > lohead = null, lotail = null;
Node<K, V> hiHead = null, hiTail = null;

Note: these two methods in HashMap are logically complex. After understanding these two methods, the get method and remove method can also be understood. As for the relevant source code of red black tree, I will write a relevant summary later.

If this [article] is helpful to you, I hope I can praise the blogger 👍, Creation is not easy. Later, I will continue to update articles such as the construction of Redis cluster and the interview questions of Redis. Let's continue to study together.

Topics: Java data structure Interview