HashMap简介(jdk1.8) 在jdk1.8中,HashMap底层由数组+链表+红黑树来实现,性能较之前有了较大的提升。如下为HashMap的继承体系结构:1 2 public class HashMap <K ,V > extends AbstractMap <K ,V > implements Map <K ,V >, Cloneable , Serializable
在这里,AbstractMap已经实现了Map接口,再实现一遍并没有任何用处,java集合框架的创始人也承认其为一个小失误。
HashMap中,当链表节点较多时会转为红黑树进行存储,而红黑树这一数据结构涉及的知识点过多,关于红黑树的基础知识需要另外学习,本篇将以链表为主,红黑树为辅的形式分析其源码。
属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 /** * 默认的初始化容量为16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 最大容量 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认负载因子为0.75 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 链表转红黑树的阈值,当有8个节点的时候转换 */ static final int TREEIFY_THRESHOLD = 8; /** * 红黑树转链表的阈值,当有6个节点的时候转换 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 转红黑树时table的最小容量,如果当前容量小于64则进行扩容而非转换 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * 基本hash节点 */ 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; } }
注意:在HashMap中,table的容量只为2的n次方。
构造函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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); } public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; } public HashMap (Map<? extends K, ? extends V> m) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false ); }
在构造函数中,并没有对table数组进行初始化,而是在第一次put的时候进行初始化,这会在下文进行详细介绍。
tableSizeFor tableSizeFor方法的主要功能是返回一个比给定整数大且最接近的2的幂次方整数,如给定10,返回2的4次方16。
1 2 3 4 5 6 7 8 9 10 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 ; }
这个方法比较巧妙,n|=n>>>1这一操作确保第一次出现1的位及其后一位(也就是头两位)都是1,而n |= n >>> 2确保头四位都是1,n |= n >>> 4确保头八位都是1,以此类推,一直到n |= n >>> 16结束后就能确保第一次出现1的位及其后面所有位都为1。而此时,n+1即为最接近指定容量的2的幂次方整数。举个例子:1 2 3 4 5 6 7 8 9 n: 0000 0000 0110 0001 = 97 n|=n>>>1: 0000 0000 0111 0001 n|=n>>>2: 0000 0000 0111 1001 n|=n>>>2: 0000 0000 0111 1101 n|=n>>>4: 0000 0000 0111 1111 ... n|=n>>>16: 0000 0000 0111 1111 n+1: 0000 0000 1000 0000 = 128
hash 1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
这个方法先得到key的hashCode,然后将其与高16位进行异或运算重新得到哈希值,之后再通过hash & (table.length - 1)
定位到key在table中的索引位置。假设table的长度为16,具体过程如下:
1 2 3 4 5 h = key.hashCode(): 1111 1111 1111 1111 0000 0000 0011 0101 h >>> 16: 0000 0000 0000 0000 1111 1111 1111 1111 hash = h ^ (h >>> 16): 1111 1111 1111 1111 1111 1111 1100 1010 table.length - 1: 0000 0000 0000 0000 0000 0000 0000 1111 hash & (table.length - 1):0000 0000 0000 0000 0000 0000 0000 1010
其中,>>>为无符号右移,左边都将补0,而之所以要进行这一步,是为了当table的值很小时,能让hashCode的高位也参与运算,以减少碰撞的几率 ,否则仅在高位发生变化总是会发生碰撞。
我们知道,hash如果对table.length取模将得到key在table长度范围内的索引位置,但由于模运算效率较低,这里便采用了与运算进行优化,提高了效率 。
get 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 && (n = tab.length) > 0 && (first = tab[(n - 1 ) & hash]) != null ) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null ) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null ); } } return null ; }
getTreeNode 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 final TreeNode<K,V> getTreeNode (int h, Object k) { return ((parent != null ) ? root() : this ).find(h, k, null ); } final TreeNode<K,V> root () { for (TreeNode<K,V> r = this , p;;) { if ((p = r.parent) == null ) return r; r = p; } } final TreeNode<K,V> find (int h, Object k, Class<?> kc) { TreeNode<K,V> p = this ; do { int ph, dir; K pk; TreeNode<K,V> pl = p.left, pr = p.right, q; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if (pl == null ) p = pr; else if (pr == null ) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null ) && (dir = compareComparables(kc, k, pk)) != 0 ) p = (dir < 0 ) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null ) return q; else p = pl; } while (p != null ); return null ; }
在这个方法中有些人可能会疑虑在同一个索引位置下的红黑树各节点hash值不应该相同吗,为什么还会有判断哈希值大小进入左右节点的操作。其实,不同的hash值在与table的长度相与后,是有可能进入同一个索引位置下的,考虑以下这种情况:1 2 3 节点1的hash值: 1110 0000 0000 1000 0111 节点2的hash值: 1001 1111 0000 1010 0111 table.length-1:0000 0000 0000 0000 0111
可以看出,节点1与节点2在进行了hash & (table.length - 1)
后值都为0000 0000 0000 0000 0111
,因此会放置在table中同一个索引位置下。
comparableClassFor、compareComparables comparableClassFor方法判断对象x所属类c是否实现了Comparable接口,如果实现了则返回所属类c,否则返回null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static Class<?> comparableClassFor(Object x) { if (x instanceof Comparable) { Class<?> c; Type[] ts, as; Type t; ParameterizedType p; if ((c = x.getClass()) == String.class ) // bypass checks return c ; if ((ts = c.getGenericInterfaces()) != null ) { for (int i = 0 ; i < ts.length; ++i) { if (((t = ts[i]) instanceof ParameterizedType) && ((p = (ParameterizedType)t).getRawType() == Comparable.class ) && //如果该泛型接口只有一个泛型参数,且此泛型参数类型为c ,则返回c (as = p.getActualTypeArguments()) != null && as.length == 1 && as[0 ] == c) return c; } } } return null ; }
以上代码中,for (int i = 0; i < ts.length; ++i)
下的一系列判断其实就是想要看x所属类c是否实现了Comparable<c>
。
1 2 3 4 static int compareComparables (Class<?> kc, Object k, Object x) { return (x == null || x.getClass() != kc ? 0 : ((Comparable)k).compareTo(x)); }
此方法中,如果x与k的类相同,则进行比较。否则,返回0。
put 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public V put (K key, V value) { 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 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
putTreeVal 在进行红黑树的操作时,依然会维护链表的结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 final TreeNode<K,V> putTreeVal (HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) { Class<?> kc = null ; boolean searched = false ; TreeNode<K,V> root = (parent != null ) ? root() : this ; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk; if ((ph = p.hash) > h) dir = -1 ; else if (ph < h) dir = 1 ; else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if ((kc == null && (kc = comparableClassFor(k)) == null ) || (dir = compareComparables(kc, k, pk)) == 0 ) { if (!searched) { TreeNode<K,V> q, ch; searched = true ; if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null ) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null )) return q; } dir = tieBreakOrder(k, pk); } TreeNode<K,V> xp = p; if ((p = (dir <= 0 ) ? p.left : p.right) == null ) { Node<K,V> xpn = xp.next; TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); if (dir <= 0 ) xp.left = x; else xp.right = x; xp.next = x; x.parent = x.prev = xp; if (xpn != null ) ((TreeNode<K,V>)xpn).prev = x; moveRootToFront(tab, balanceInsertion(root, x)); return null ; } } }
resize 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 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 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0 ) { float ft = (float )newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float )MAXIMUM_CAPACITY ? (int )ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings ({"rawtypes" ,"unchecked" }) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null ) { for (int j = 0 ; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) newTab[e.hash & (newCap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , newTab, j, oldCap); else { Node<K,V> loHead = null , loTail = null ; Node<K,V> hiHead = null , hiTail = null ; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0 ) { if (loTail == null ) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null ) hiHead = e; else hiTail.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; }
我们可以看到在此方法中有一个条件判断else if (oldThr > 0)
,当table为空但阈值却大于零时,将阈值赋值给新容量。这里有个疑问是为什么会发生table为空但阈值却大于零这种情况?我们可以回过头看看构造函数,可以发现在所有构造函数中都没有对数组table进行过分配,而仅仅设置了阈值this.threshold = tableSizeFor(initialCapacity);
,既然在构造时没有分配,那肯定就是在第一次扩容时分配的,也就正是上面的代码。
此处还有一个疑问是:为什么扩容后新的存储位置只为原位置或原位置+原容量?请看这么一个例子,假设oldCap=0100, newCap=1000,节点a的hash为1110,节点b的hash为1010。oldCap-1的值为0011,显然对于节点来说只有后两位决定了它们的位置(因为前两位无论如何都为0),而newCap-1的值为0111,此时后三位决定了它们的位置,与之前不同正在于节点的第三位是0还是1,而第三位的值正可以通过oldCap(在此也就是0100)相与来进行判断,如果相与结果为0000,则说明第三位的值为0,在和newCap-1相与后结果将不变,依然在原索引位置;而如果相与结果为0100,则说明节点第三位值是1,也就是原索引值加上原容量。
treeifyBin 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 final void treeifyBin (Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1 ) & hash]) != null ) { TreeNode<K,V> hd = null , tl = null ; do { 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 ) hd.treeify(tab); } } TreeNode<K,V> replacementTreeNode (Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
treeify 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 final void treeify (Node<K,V>[] tab) { TreeNode<K,V> root = null ; for (TreeNode<K,V> x = this , next; x != null ; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null ; if (root == null ) { x.parent = null ; x.red = false ; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null ; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1 ; else if (ph < h) dir = 1 ; else if ((kc == null && (kc = comparableClassFor(k)) == null ) || (dir = compareComparables(kc, k, pk)) == 0 ) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0 ) ? p.left : p.right) == null ) { x.parent = xp; if (dir <= 0 ) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); break ; } } } } moveRootToFront(tab, root); }
treeify方法用来构建一棵以调用该方法的节点为根节点的红黑树。由于红黑树依然维护着链表结构,每次通过next属性获得下一个节点时,都会从根节点开始向下查找,根据hash值的大小找到合适的位置放入,并设置好parent与left或right属性以关联节点。
remove 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 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 ) { Node<K,V> node = null , e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null ) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break ; } p = e; } while ((e = e.next) != null ); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this , tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null ; }
常见问题 有关HashMap的常见面试题总结请移步 HashMap常见面试题总结