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.