HashMap
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable { }
1. Some important parameters
1.1 serialVersionUID attribute
// Serialized version number private static final long serialVersionUID = 362498820763181265L;
SerialVersionUID is suitable for Java serialization mechanism. In short, the mechanism of Java serialization is to verify the consistency of the version by judging the serialVersionUID of the class. During deserialization, the JVM will compare the serialVersionUID in the transmitted byte stream with the serialVersionUID of the corresponding local entity class. If the same description is consistent, it can be deserialized. Otherwise, an exception with the same deserialization version will appear, that is, InvalidCastException.
**The specific serialization process is as follows: * * during serialization, the serialVersionUID of the current class of the system will be written to the serialization file. When deserializing, the system will automatically detect the serialVersionUID in the file to determine whether it is consistent with the serialVersionUID in the current class. If it is consistent that the version of the serialized file is the same as that of the current class, the deserialization can succeed, otherwise it will fail;
1.2 DEFAULT_ INITIAL_ Capability attribute
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
DEFAULT_ INITIAL_ Capability is the default initial capacity of HashMap, with a size of 16
1.3 DEFAULT_LOAD_FACTOR attribute
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
DEFAULT_LOAD_FACTOR is the default load factor size of HashMap, which is 0.75.
When the number of elements ≥ capacity * load factor, HashMap needs to be expanded.
1.4 MAXIMUM_ Capability attribute
/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30;
MAXIMUM_ The capability property is the maximum capacity of the HashMap.
1.5 TREEIFY_THRESHOLD property
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8;
TREEIFY_ The threshold attribute is the threshold for the transformation of HashMap into a red black tree. When the number of elements is greater than this value, it will be transformed into a red black tree.
1.6 UNTREEIFY_THRESHOLD property
UNTREEIFY_THRESHOLD is the threshold value for the transformation of HashMap from red black tree to linked list. When the number of elements is greater than this value, it will be transformed into a linked list.
1.7 MIN_ TREEIFY_ Capability attribute
MIN_ TREEIFY_ Capability is the minimum array capacity that can transform a linked list into a red black tree. Without this limitation, if there are too many nodes, the node array will be resize d frequently.
2. Some important attributes
2.1 table attribute
/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node<K, V>[] table;
The table attribute is used to store nodes.
2.2 entrySet attribute
/** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */ transient Set<Map.Entry<K, V>> entrySet;
The entrySet property is used to cache a collection of key value pairs.
2.3 size attribute
/** * The number of key-value mappings contained in this map. */ transient int size;
The size attribute is used to store the number of key value pairs of the map.
2.4 modCount attribute
/** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). */ transient int modCount;
The modCount property records the number of times the map structure has been modified for quick failures in the iterator.
2.5 threshold attribute
/** * The next size value at which to resize (capacity * load factor). * * @serial */ // (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) int threshold;
The threshold attribute records the next size that needs to be resize d
2.6 loadFactor attribute
/** * The load factor for the hash table. * * @serial */ final float loadFactor;
The loadFactor attribute records the load factor of the current map
2.7 Node internal class
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; } }
The Node internal class actually represents a key value pair and contains a hash value and a next pointer.
2.8 EntrySet inner class
final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<Map.Entry<K,V>> iterator() { return new EntryIterator(); } public final boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Node<K,V> candidate = getNode(hash(key), key); return candidate != null && candidate.equals(e); } public final boolean remove(Object o) { if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Object value = e.getValue(); return removeNode(hash(key), key, value, true, true) != null; } return false; } public final Spliterator<Map.Entry<K,V>> spliterator() { return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer<? super Map.Entry<K,V>> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (Node<K,V> e : tab) { for (; e != null; e = e.next) action.accept(e); } if (modCount != mc) throw new ConcurrentModificationException(); } } }
The EntrySet internal class is used to represent the key collection of the current HashMap.
3. Some tools and methods
3.1 hash method
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
The (H = key. Hashcode()) ^ (H > > > 16) process of this hash() method is worth studying.
First, H > > > 16 moves the hash value unsigned to the right by 16 bits, which is equivalent to taking the high 16 bits of the original value.
0000 0100 1011 0011 1101 1111 1110 0001 >>> 16
Get:
0000 0000 0000 0000 0000 0100 1011 0011
Then, let's look at an indexFor() method in JDK 1.7. After JDK8, a large number of * * tab [(n - 1) & hash] * * forms appear in the source code, which is actually the same.
static int indexFor(int h, int length) { return h & (length-1); }
The return value of the indexFor() method is the array index we want to find in the table array. However, in most cases, the length is generally less than 2 ^ 16, that is, less than 65536. Therefore, the result of return h & (length-1) is always the lower 16 bits of h and (length-1).
So obviously, every time we calculate the subscript, we can only use the low order. If we find a way to make use of high bits, we can increase the degree of hashing. Therefore, the hash() method selects XOR with its own high sixteen bits (H > > > 16) to make use of the high bits.
3.2 tableSizeFor method
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
The tableSizeFor method is used to calculate the resize threshold corresponding to the parameter cap. It often appears as the following statement.
threshold = tableSizeFor(size);
3.3 resize method
final Node<K, V>[] resize() { // Get the old array, capacity and expansion threshold Node<K, V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) {// If the old capacity is not 0 if (oldCap >= MAXIMUM_CAPACITY) {// If the old capacity has reached the maximum capacity // Set the capacity expansion threshold to an impossible maximum threshold = Integer.MAX_VALUE; // Returns the old array directly return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY //If the maximum capacity is not reached after the old capacity is doubled, set it as the new capacity && oldCap >= DEFAULT_INITIAL_CAPACITY)// And the old capacity is greater than or equal to the initial capacity // Double the new capacity expansion threshold newThr = oldThr << 1; } else if (oldThr > 0) // If the old capacity is 0 and the old capacity expansion threshold is greater than 0 // Then let the new capacity become the old capacity expansion threshold newCap = oldThr; else { // Both old capacity and old capacity expansion thresholds are 0 // Make the new capacity the default initial capacity, and calculate the expansion threshold newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {// If the new capacity expansion threshold is 0 // Calculate current capacity * load factor float ft = (float) newCap * loadFactor; // Set the new capacity expansion threshold to ft or Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE); } // Update capacity expansion threshold threshold = newThr; // Create a new table with the size of the new capacity calculated previously @SuppressWarnings({"rawtypes", "unchecked"}) Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; // Update table pointer table = newTab; if (oldTab != null) {// If the old array is not null for (int j = 0; j < oldCap; ++j) {// Ergodic Node<K, V> e; if ((e = oldTab[j]) != null) {// If the old array value at j is not null, save it in e // Set the value in the old array to null oldTab[j] = null; if (e.next == null)// If e.next is null // Find and assign values in the new array newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode)// If e this is a tree, call split to split the tree and bring it to the new array ((TreeNode<K, V>) e).split(this, newTab, j, oldCap); else { // E here is a linked list, and e.next is not null // The following is a linked list segmentation operation, which divides the original linked list into lo string and hi string. Node<K, V> loHead = null, loTail = null; Node<K, V> hiHead = null, hiTail = null; Node<K, V> next; do { // Save next next = e.next; /* Whether e.hash & oldcap is 0 or 1 can determine whether e should be on the original string or on the new string. hashMap Before and after capacity expansion, the capacity size is a power of 2. If the binary form of the original capacity size is 1000 and the binary form of the new capacity is 10000 Since the calculation formula of array subscript position is e.hash & (size-1), it was hash & 0111 and then hash & 01111. In fact, the higher bit of the hash is used to determine the position. Then hash & size can know whether the high bit in the hash value is 0 or 1 If the higher bit is 0, the new position is the same as the original position. If the higher bit is 1, move the position. */ if ((e.hash & oldCap) == 0) {// If the position does not change // Add e to lo string if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else {// If the position needs to be changed // Add e to hi string if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null);// If e is assigned to next, it is not null // Place the lo string at the position with subscript j, that is, the unchanged position if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // Put the hi string at the subscript j+oldCap, because the higher bit in the hash of the new position is 1, then the subscript needs to add an oldCap if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
First, try to determine the new capacity and new expansion threshold by mathematical method.
Then traverse the array and deeply traverse the linked list / red black tree. If it is a linked list, split it into lo linked list and hi linked list.
3.4 treeifyBin method
final void treeifyBin(Node<K, V>[] tab, int hash) { int n, index; Node<K, V> e; // If the array is empty or small, resize() if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) {// If the position corresponding to the hash is not null in the array, you can enter this loop TreeNode<K, V> hd = null, tl = null; do { // Transform node e into a tree node and store it in variable p 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) // Turn into red black tree hd.treeify(tab); } }
treeifyBin method transforms the qualified linked list into a red black tree.
4. Some business methods
4.1 get method
public V get(Object key) { Node<K, V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K, V> getNode(int hash, Object key) { Node<K, V>[] tab; Node<K, V> first, e; int n; K k; if ((tab = table) != null // table is not empty && (n = tab.length) > 0 // table length is not 0 && (first = tab[(n - 1) & hash]) != null) {// The Node obtained at the calculated position is not null. This is the chain header if (first.hash == hash // Check the hash value of first. If it is correct and the key matches && ((k = first.key) == key || (key != null && key.equals(k)))) return first;// Directly return to first if ((e = first.next) != null) {// If the length of the first linked list is more than 1 if (first instanceof TreeNode) {// If the current position of the table extends a red black tree return ((TreeNode<K, V>) first).getTreeNode(hash, key);// Call the getTreeNode method } // It's not a red black tree, it's a linked list. Just traverse and find it do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
See note above for details. Calculate the position in the array with the hash value, and then find it along the linked list or red black tree.
4.2 test method
public V put(K key, V value) { // Call putVal return putVal(hash(key), key, value, false, true); } 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) // If the table is null or empty, expand the capacity first, and then assign the length to n n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) // If the specified location found is empty, newNode is called here with a new key value pair tab[i] = newNode(hash, key, value, null); else { // If the location found is not empty Node<K, V> e; K k; if (p.hash == hash // If the hash value matches && ((k = p.key) == key || (key != null && key.equals(k))))// And the key matches // Assign the current node p to e e = p; else if (p instanceof TreeNode)// If the current p is a tree node, call the putTreeVal method e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); else {// If it is currently a linked list node for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {// Let e=p.next if it is null // Making p.next a newNode created with a new key value pair inserts the new node at the end of the linked list p.next = newNode(hash, key, value, null); // If the length reaches the minimum threshold for transforming into a red black tree, it will be transformed into a red black tree if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // If you find it if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // Let p be e, e=p.next at the beginning of the for loop, so it is equivalent to p moving backward p = e; } } if (e != null) { // If the key already exists, update it and return oldValue V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // Check whether capacity expansion is required if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
See note above for details. Calculate the position in the array with the hash value, and then search in the linked list or red black tree to update or newNode.
4.3 remove method
public V remove(Object key) { Node<K, V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } final Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K, V>[] tab; Node<K, V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {// Check whether there is a linked list / tree at the corresponding array position Node<K, V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // If the chain header is, assign it to node node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) // If it is a tree, call the corresponding method to find it and assign it to node node = ((TreeNode<K, V>) p).getTreeNode(hash, key); else { // If it's a linked list, go through the search do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { // Found, assign the current node to node node = e; // Exit the loop, which causes the following p=e not to be executed when it is found break; } // Assign the current node to p p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) // If it is a tree, delete it with the corresponding method ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable); else if (node == p) // If you want to remove the chain header, according to the logic in the do while block above, node==p, we will directly start the chain list from node.next tab[index] = node.next; else // If the chain header is not to be removed, follow the logic node in the do while block above= p. Before p - > node, delete node p.next = node.next; // Handling modCount and size attributes ++modCount; --size; afterNodeRemoval(node); // Returns the remove d node return node; } } return null; }
Find - > delete, very clear logic
4.4 clone method
@Override public Object clone() { HashMap<K,V> result; try { result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } result.reinitialize(); result.putMapEntries(this, false); return result; }
The clone() method makes a shallow copy of the HashMap.
5. Some methods from JDK8
The following method comes from rewriting the method in the Map interface in JDK8.
Some functional methods, such as compute() and merge(), are not parsed here.
5.1 getOrDefault / putIfAbsent
@Override public V getOrDefault(Object key, V defaultValue) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? defaultValue : e.value; } @Override public V putIfAbsent(K key, V value) { return putVal(hash(key), key, value, true, true); }
It is well understood to get the actual value or default value / insert when it does not exist.
5.2 remove / replace
@Override public boolean remove(Object key, Object value) { return removeNode(hash(key), key, value, true, true) != null; } @Override public boolean replace(K key, V oldValue, V newValue) { Node<K,V> e; V v; if ((e = getNode(hash(key), key)) != null && ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) { e.value = newValue; afterNodeAccess(e); return true; } return false; } @Override public V replace(K key, V value) { Node<K,V> e; if ((e = getNode(hash(key), key)) != null) { V oldValue = e.value; e.value = value; afterNodeAccess(e); return oldValue; } return null; }
When the corresponding key value pair exists, delete \ replace it.