The class diagram
First, take a look at the four commonly used implementation classes of the Map family: HashMap, Hashtable, LinkedHashMap, and TreeMap.
Below we mainly interpret HashMap, combined with the source code, from the storage structure, common method analysis, expansion and security and other aspects of in-depth interpretation of the working principle of HashMap.
Storage structure
In terms of structure, HashMap is an array + linked list + red-black tree (JDK1.8 added red-black tree), as shown below:
Red-black trees were introduced because the average time complexity for finding, inserting, and deleting is order (log(n)). This is because when hash collisions occur, the data is mounted (tailed) to form a linked list. The linked list is discontinuous in space and continuous in logic. It is fast to add and delete elements, and only needs to process references between nodes. The time complexity is O(1), and the query is slow.
Internal properties
- Default initial capacity DEFAULT_INITIAL_CAPACITY = 1 << 4 (16)
- Maximum capacity MAXIMUM_CAPACITY = 1 << 30
- Default load factor DEFAULT_LOAD_FACTOR = 0.75f
- Threshold = Capacity x load factor
- The threshold for converting to a red-black tree: TREEIFY_THRESHOLD = 8
- UNTREEIFY_THRESHOLD = 6
A constructor
// The default constructor, with the default load factor of 0.75f
public HashMap(a) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
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;
this.threshold = tableSizeFor(initialCapacity);
}
// The tableSizeFor function (not considering the case of larger than maximum capacity) is to return a number greater than the input parameter and the nearest integer power of 2.
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0)?1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
Copy the code
The main method
Index index calculation
The Hash algorithm here is essentially a three-step process: taking the hashCode value of the key, the high-order operation, and the modulus operation.
// Perturbation function
static final int hash(Object key) {
int h;
// >>> 16 Unsigned move 16 bits to the right to reduce collisions
return (key == null)?0 : (h = key.hashCode()) ^ (h >>> 16);
}
index = (n - 1) & hash;
Copy the code
The index is calculated by (n-1) & hash, that is, the binary of (array length -1) and the hash value calculated previously.
(hash%length==hash&(length-1) if length is 2 to the NTH power;)
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;
// Check whether the current array is initialized
if ((tab = table) == null || (n = tab.length) == 0)
// If not, call the resize method to initialize (default length is 16).
n = (tab = resize()).length;
// (n-1) &hash determines which bucket the element is in
if ((p = tab[i = (n - 1) & hash]) == null)
// If the bucket is empty, the new generated node is put into the array
tab[i] = newNode(hash, key, value, null);
// If an element already exists in the array
else {
Node<K,V> e; K k;
// If it is equal to the hash value of the first element, the key is equal
if(p.hash == hash && ((k = p.key) == key || (key ! =null && key.equals(k))))
// Assign the first element to e, which is recorded as e
e = p;
// If the node is a red-black tree node
else if (p instanceof TreeNode)
// Put it in the tree
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// The rest is a linked list node
else {
// Insert the node at the end of the list.
// Iterate through the current list
for (int binCount = 0; ; ++binCount) {
// If p.next is empty, it is at the end of the list
if ((e = p.next) == null) {
// Insert the new node at the end
p.next = newNode(hash, key, value, null);
// If the number of nodes reaches the threshold of 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// Convert to a red-black tree
treeifyBin(tab, hash);
// Break out of the loop
break;
}
// Determine whether the key value of the node in the list is equal to the key value of the inserted element
if(e.hash == hash && ((k = e.key) == key || (key ! =null && key.equals(k))))
// If yes, break out of the loop
break;
// Use to traverse the list in the bucket. Combine with the previous e = p.ext to traverse the listp = e; }}// e has a record, which represents the node whose key value and hash value of the element found in the bucket are equal to the inserted element
if(e ! =null) {
// Record the value of e (first element)
V oldValue = e.value;
// onlyIfAbsent if true, don't change existing value
// The value of onlyIfAbsent is false or null
if(! onlyIfAbsent || oldValue ==null)
// Replace the old value with the new value
e.value = value;
// Call back after access to add the element to the end of the list
afterNodeAccess(e);
// Return the old value
// map.put(1, "Orcas");
// String oldVal = map.put(1, "Fish");
// => Orcas
returnoldValue; }}// The record is the number of internal structural changes
++modCount;
// Put an element ++size each time. When the actual size exceeds the threshold, expand the capacity
if (++size > threshold)
resize();
// Callback after insert
afterNodeInsertion(evict);
return null;
}
Copy the code
The specific process can be understood by the following figure:
Expansion mechanism
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 there are elements in the old array, it means that the array has been initialized
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
// If the size of the old array is larger than the maximum capacity of 2^30, set the threshold to the maximum value of Integer
threshold = Integer.MAX_VALUE;
return oldTab;
}
// The old array is doubled to less than the maximum capacity and the old array is larger than the default initial capacity 16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// Set threshold*2 to get the new threshold
newThr = oldThr << 1;
}
// Old threshold =threshold greater than 0
// Use the constructor HashMap(int initialCapacity, float loadFactor)
// In this method, this.threshold = tableSizeFor(initialCapacity);
// tableSizeFor returns the capacity of the array (2^N)
else if (oldThr > 0)
// Set the capacity to threshold
newCap = oldThr;
else {
// The threshold is 0 when initialized, oldCap is empty, that is, the array was created with no parameters, and resize() is called to initialize to the default value
// Set the new length to the default initialization length, which is 16
newCap = DEFAULT_INITIAL_CAPACITY;
// The load factor is 0.75* Array length 16=12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// If the new threshold is 0, set the new threshold based on the load factor
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// Create a new array of nodes with length newCap
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// If the old array contains data, copy the array into the new array
if(oldTab ! =null) {
// Loop through the old array, copying the nodes with elements
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// The old array has element nodes
if((e = oldTab[j]) ! =null) {
oldTab[j] = null;
/ / array
if (e.next == null)
// Recalculate the hash value to determine the element position
newTab[e.hash & (newCap - 1)] = e;
/ / the red-black tree
else if (e instanceof TreeNode)
// Split the original binary tree structure into a new red-black tree
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/ / list
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// If it is 0, the position of the element in the expanded array has not changed
if ((e.hash & oldCap) == 0) {
if (loTail == null)
/ / the first
loHead = e;
else
loTail.next = e;
loTail = e;
}
// If it is not 0, the position of the element in the array is changed after the expansion, and the new subscript is the original index +oldCap
else {
if (hiTail == null)
hiHead = e;
elsehiTail.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;
}
Copy the code
As shown in the following figure, n is the length of the table, and array expansion n << 1. Figure (a) shows the index location determined by key1 and key2 before capacity expansion. Figure (b) shows the index location determined by key1 and key2 after capacity expansion. Therefore, when we extend the HashMap, we don’t need to recalculate the hash as in the JDK1.7 implementation. We just need to see if the new bit is 1 or 0. If 0 is the same index, the index becomes “old index +oldCap”.