HashMap简述
HashMap是工作中最常用的集合工具之一,在整个集合框架中也是很重要的一部分,因此本篇文章主要讲述它的底层实现原理,因为jdk1.8中对HashMap的数据结构有了修改,所以本篇将会分别讲解jdk1.7和jdk1.8中HashMap的区别,通过对比学习来加深对HashMap的理解
jdk1.8之前HashMap采用【数组+链表】实现,使用链表处理hash冲突,同一个hash值都存在一个链表里。但是当存储的元素较多时,hash值相等的元素也会增多,通过key值依次查找的效率就降低了许多。
jdk1.8中,HashMap采用【数组+链表+红黑树】实现,当链表长度超过8时,将链表转换为红黑树,这样就大大提高的查找的时间
HashMap数据结构
JDK1.7中HashMap数据结构
HashMap中的数组即为嵌套类Entry,数组中的每个元素是一个单项链表,每个Entry包含四个属性:
🌂key, value, hash 值和用于单向链表的 next。
🌂capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
🌂loadFactor:负载因子,默认为 0.75。
🌂threshold:扩容的阈值,等于 capacity * loadFactor
1 2 3 4 5 6 7
| static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; ..................................... }
|
JDK1.8中HashMap数据结构
Java8 对 HashMap 做了一些修改,最大的不同就是利用了红黑树,所以其由 【数组+链表+红黑树】 组成。
我们知道,Java7 HashMap查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。
为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在进行查找的时候可以降低时间复杂度为 O(logN)。
Java7 中使用 Entry 来代表每个 HashMap 中的数据元素,Java8 中使用 Node,都是** key,value,hash** 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; } ............................................... }
|
HashMap存取值分析
HashMap存值(put)分析【JDK1.7】
HashMap存值源码走读【JDK1.7】
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
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
| public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
modCount++; addEntry(hash, key, value, i); return null; }
|
HashMap数组初始化【JDK1.7】
当第一个元素插入时,会初始化数组,计算数组的初始化大小及扩容阈值
1 2 3 4 5 6 7 8 9 10
| private void inflateTable(int toSize) { int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
|
HashMap元素存放位置计算【JDK1.7】
使用key的hash值与数组长度大小减一取模
1 2 3 4
| static int indexFor(int h, int length) { return h & (length-1); }
|
HashMap添加元素到链表【JDK1.7】
先判断key值是否重复,如果不重复添加新元素到链表,添加之前先判断是否需要扩容,如果需要则先扩容,重新计算hash,然后再将新元素存入对应链表的表头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
|
HashMap数组扩容【JDK1.7】
在插入新值的时候,如果当前的 size 已经达到了阈值,并且要插入的数组位置已经有元素,就会触发扩容,扩容后,数组大小为原来的 2 倍。
扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值迁移到新的数组中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
|
JDK1.7取值(get)分析
相对于存值(put),HashMap取值相对简单,大致逻辑为:
🌂根据key计算hash
🌂使用key的hash值与数组长度取模(length-1)得到元素存储位置下标
🌂遍历数组中在该下标处的链表,直到找到相等的key
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
| public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue(); } private V getForNullKey() { if (size == 0) { return null; } for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
|
HashMap存值(put)分析【JDK1.8】
HashMap存值(put)源码走读【JDK1.8】
相对于jdk1.7中HashMap存值,jdk1.8的逻辑相对复杂,因为需要判断数据节点类型是链表还是红黑树,然后使用对应的方法进行查找
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
| 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; }
|
HashMap数组扩容【JDK1.8】
resize() 方法用于初始化数组或数组扩容,初始化时使用默认容量(16)和默认阈值(0.74*16),后来扩容时,容量为原来的 2 倍,并进行数据迁移
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
| 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; 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; }
|
JDK1.8取值(get)分析
取值逻辑如下:
1)计算 key 的 hash 值,根据 hash 值计算对应数组下标: hash & (length-1)
2)判断数组该位置处的key是否相等,如果不等,走第(3)步,否则结束
3)判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第(4)步
4)遍历链表,直到找到相等(==或equals)的 key
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
| 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; }
|