HashMap principle, source code analysis

This article mainly combines its own practical application and source code analysis to analyze the implementation principle of HashMap.
Main analysis content :

  The hash distribution logic of HashMap, the processing of hash collision, the triggering of expansion, and treeing, etc.
Text order :

  1. Combine the source code, analyze the basic attributes and content
  2. Compare the source code with the example

Let's first talk about the basic properties and principles:

// The default capacity is 16
 // When the first put() operation is performed, resize() will be called to initialize the table array, and the (table) array
 // will be introduced below. The length of the array is this default parameter 
static  final  int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; 
 // Maximum capacity 
static  final  int MAXIMUM_CAPACITY = 1 << 30 ;
 // This is a coefficient, as long as you know that the conditions for triggering expansion are:
 // Use capacity > Current total capacity * DEFAULT_LOAD_FACTOR 
static  final  float DEFAULT_LOAD_FACTOR = 0.75f ​​;
 // The condition for triggering treeization, at first, when there is the same hash array to the same table array position i
 // then it is stored in the form of a linked list at this position, When it is greater than this length, it is converted into a tree 
static  final  int TREEIFY_THRESHOLD = 8;
 // This is the real body of HashMap, it is actually an array, and the element is Node class.
// Node is an inner class, the following will introduce 
transient Node<K,V> [] table;


// Node<K,V> class
 // This class is actually the basic form of HashMap element storage
 // And Node can form a linked list, which is stored in the i-th position of the table in the form of a linked list 
static  class Node<K,V> implements Map.Entry<K,V> {
         final  int hash;
         final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

Then let's look at the logic of the put() operation

When put() is operated, it is necessary to deal with possible scenarios such as hash collision, expansion, and treeing. It is also the core of this analysis.

//put 操作
public V put(K key, V value) {
       //调用putValue 方法      
        return putVal(hash(key), key, value, false, true);
}

// hash method, you can ignore 
static  final  int hash(Object key) {
         int h;
         return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 );
}

// Core 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;
         // Generally speaking, the first put will enter this if condition, and then call the resize method 
        if ((tab = table) == null || (n = tab.length) == 0 )
            n = (tab = resize()).length;
         // Hash to the table array according to the hash, if it is found that there is no
         // fill object in this position of the array, create a new Node directly and fill 
        if ((p = tab[ i = (n - 1) & hash]) == null )
            tab[i] = newNode(hash, key, value, null );
         else {
          // Processing here, when the hash value is the same and hashed to the same position of the table array
          // processing logic. 
            Node<K,V> e; K k;
          // Well, even if your hash value is the same, you don't have to be inserted after me
          // unless neither == nor equals. Otherwise, the current node can only be replaced 
            if (p.hash == hash && 
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
             // If it goes out at the i-th position of the table, the extended linked list has been converted into a tree.
            // Then execute the tree insertion operation 
            else  if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal( this , tab, hash, key, value);
             else {
               // Then, this piece is mainly to the i-th position of the table, and the extended linked list is at the end
               / / Insert a new node. Through the for loop, reach the end of the linked list 
                for ( int binCount = 0; ; ++ binCount) {
                     if ((e = p.next) == null ) {
                        p.next = newNode(hash, key, value, null );
                         // If the length of the linked list has reached the treeify threshold, then treeify 
                        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 the storage capacity > threshold, then trigger the expansion
        // threshold = current capacity * DEFAULT_LOAD_FACTOR (parameters described above) 
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  

 

Some basic zodiac signs and methods have been introduced above.

Then introduce the storage structure of HashMap:

First of all, it has been introduced above that the Node<K,V> class is the basic structure type stored by HashMap.

HashMap is actually an array of Node type.

Stored procedure:

1. Allocate capacity first

2. According to the hash value array

  2.1. When the hash array is calculated to the position n of the array, if the table[n] does not store the object at the beginning ( table[n] == null), it is directly stored in new Node<K,V>

  2.2. If table[n] stores objects (table[n] != null). In this case, the hashCode and equals methods of K should be eliminated to determine whether to update the current node or insert it at the end.

    If there is a discrepancy, judge whether the treeing threshold is reached. Tree if reached. If it is not reached, it will enter and exit at the end of the linked list.

3. Determine the capacity after each insertion and whether to expand the capacity

 

Then a specific simple example:

By overriding the hashCode and equals methods, hash collisions are generated, and then treeing is triggered

package temp;

import java.util.HashMap;
import java.util.Map;

public class HashMapDemo {
    public static void main(String[] args) {
        Map<MyValue,Object> hashMap = new HashMap<MyValue, Object>();
        hashMap.put(new MyValue("one"),"first");
        hashMap.put(new MyValue("two"),"secend");
        for(int i=0;i<18;i++){
            hashMap.put(new MyValue("dump"),"dump");
        }
    }

    private static class MyValue{
        String value;
        public MyValue(String value){
            this.value = value;
        }
        // Rewrite, hash to table[n] 
        public  int hashCode(){
             return value.hashCode();
        }
        // Remember the judgment method of putVal, as long as it hashes to the same position
         // and returns true through equals, it will append 
        public  boolean equals(MyValue v){
             return  false ;
        }

        public String toString(){
            return value;
        }
    }
}

Only some basic cores and principles are introduced here.
Of course, there are also the expansion method resize(), the tree method treeifyBin(), and the node removal method removeNode()

You can refer to the source code to see the specific implementation logic.

Alright, that's it.

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324694120&siteId=291194637