jdk版本1.8
主要是了解下其内部原理
一、简介(粗略翻译)
- HashMap实现了Map接口。HashMap非线程安全,并且HashMap的Key和Value都可以是null值。其它方面HashMap和HashTable比较像。HashMap不保证元素的顺序。
- 如果哈希方法适当的话,其get与put方法是固定的时间复杂度。迭代的时间复杂度是bucket数目加上桶的大小。所以如果对迭代的性能有很高的要求话,则不适合对HashMap赋予很大的存储,或者很低的load factor(装载因子),如果load factor很低,则要么capacity很大跟前面那个一样,相当于对每一个key都做一个哈希,那么当然遍历时间长了;要么size和capactiy都很低,那么就会频繁的扩容,同样也影响性能。
- 影响HashMap性能有两个主要的因素,一个是bucket数目,另一个是load factor(装载因子)。load factor用来衡量是否HashMap需要进行扩容。如果Entry的数量超过了 load factor * 当前容量,那么HashMap会进行扩容,有两个步骤一个是rehash,还有个是扩充容量,容量大约是原始容量的两倍。
- 一个通用的设定,load factor 为0.75。因为当load factor设定为0.75时,很好的权衡了时间和空间复杂度。更高的load factor减少空间开销但是增加时间复杂度(体现在很多基本方法中,其中包括get 和 put)。为了最小话rehash数量,当设置初始容量时,需要把entry数量和load factor考虑在内。如果capacity > entry_size / load factor, 则不会rehash。
- 如果在一个HashMap中有很多映射的时候,相比自动出发rehash扩容HashMap,初始化很大的capacity会有更好的性能表现,因为rehash一次就有一次性能开销,很多key拥有一样的hashcode当然会拉低HashMap的性能表现,要不然HashMap不就和链表一样了嘛?并且为了改善性能,消除打结,当key实现了Comparable,则HashMap能够利用比较来消除打结。
- HashMap并非线程安全。如果多线程对同一个HashMap进行结构化操作(增加或者删除映射,改变value不算)那么一定要在外部对HashMap进行同步。经常发生在对Map进行压缩。
- 迭代器有fast fail的特性。当一个Map在创建了Iterator之后被结构改变了,迭代器会抛出ConcurrentModificationException。因此当对HashMap进行并发的结构改变时,迭代器会快速无污染的抛出异常,因为在做改变之前就会fail。
- 由于fast fail的机制不能被保证在任何情况下都会出现,因此用这种机制写项目是错误的。正确的做法是用此机制来debug。
二、主要结构分析
(1)Node。其基本的结构,是一个静态内部类。被很多种entry都用到。注意其hashCode方法并非直接返回Hash。eqauls方法是key 和 value都相等。
1 static class Node<K,V> implements Map.Entry<K,V> { 2 final int hash; 3 final K key; 4 V value; 5 Node<K,V> next; 6 7 Node(int hash, K key, V value, Node<K,V> next) { 8 this.hash = hash; 9 this.key = key; 10 this.value = value; 11 this.next = next; 12 } 13 14 public final K getKey() { return key; } 15 public final V getValue() { return value; } 16 public final String toString() { return key + "=" + value; } 17 18 public final int hashCode() { 19 // the hash code is hashcode of key xor hash code of value. Not only hash. 20 return Objects.hashCode(key) ^ Objects.hashCode(value); 21 } 22 23 public final V setValue(V newValue) { 24 V oldValue = value; 25 value = newValue; 26 return oldValue; 27 } 28 29 //key and value are all the same then , the result is true. 30 public final boolean equals(Object o) { 31 if (o == this) 32 return true; 33 if (o instanceof Map.Entry) { 34 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 35 if (Objects.equals(key, e.getKey()) && 36 Objects.equals(value, e.getValue())) 37 return true; 38 } 39 return false; 40 } 41 }
(2)Function hash,也称为扰动函数
顺便说一下,HashMap的capicity必须是2的n次方。
1、返回Node的hash值。
2、并非直接返回key的hash值,因为key的hash值太多了。
3、计算方法为key的低16位与高16位异或,高16位不变,有人做过实验,这种做法可以减少碰撞而且高效。
4、为什么HashMap的capitity为2的幂,因为在求桶的位置时,要用长度作为掩码,即index = (n-1)&hash。因此减一就可以获得掩码,即减1位全为1。
5、如果直接用key的hash和掩码相与时,容易出现碰撞。所以用加上扰动函数产生的掩码可以减少这种情况。
1 /* 2 这是hashMap的hash方法,可以看到并不是直接拿key的hashCode 3 返回的是key的低16位hash值和高16位hash的异或,因为h右移16位后,高位是16个0,任何数和0异或都是其本身 4 5 */ 6 static final int hash(Object key) { 7 int h; 8 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 9 }
(3)Function resize。对HashMap重新分配空间
1 /** 2 * 初始化,要么double被大小 3 */ 4 final Node<K,V>[] resize() { 5 Node<K,V>[] oldTab = table; 6 //设置上一个map的capcity,记为oldCap 7 int oldCap = (oldTab == null) ? 0 : oldTab.length; 8 //老的threshold. threshold = capacity * load factor, threshold是下一次resize的阈值 9 int oldThr = threshold; 10 int newCap, newThr = 0; 11 //如果不是初始化的话 12 if (oldCap > 0) { 13 //如果老的capacity >= 最大的capactiy的话,直接把threshhold设为整数的最大值,也不resize了 14 if (oldCap >= MAXIMUM_CAPACITY) { 15 threshold = Integer.MAX_VALUE; 16 return oldTab; 17 } 18 //现将新的capacity记为newCap设为oldCap<<1即2*oldCap,然后如果小于最大的capacity并且 oldCap >= 默认的初始Capacity的话 19 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 20 oldCap >= DEFAULT_INITIAL_CAPACITY) 21 newThr = oldThr << 1; // 将threshold设为两倍 22 } 23 //如果oldCap = 0但是oldThr > 0的话,更新newCap 24 else if (oldThr > 0) // initial capacity was placed in threshold 25 newCap = oldThr; 26 //否则newCap为默认初始capacit, 更新newThread 27 else { // zero initial threshold signifies using defaults 28 newCap = DEFAULT_INITIAL_CAPACITY; 29 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 30 } 31 //经历了上述所有步骤后,如果newThread还为0的话,更新newThre 32 if (newThr == 0) { 33 float ft = (float)newCap * loadFactor; 34 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 35 (int)ft : Integer.MAX_VALUE); 36 } 37 //更新threshold 38 threshold = newThr; 39 @SuppressWarnings({"rawtypes","unchecked"}) 40 //经历了上述很多计算后,初始化了桶。容量就是newCap了。 41 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 42 table = newTab; 43 //如果老的table不为空的话,就要进行操作了,老的元素往哪放呢?接着往下看 44 if (oldTab != null) { 45 for (int j = 0; j < oldCap; ++j) { 46 Node<K,V> e; 47 //先得到老的节点,如果老节点不为空 48 if ((e = oldTab[j]) != null) { 49 //先把老节点设为空 50 oldTab[j] = null; 51 //如果老节点没有next,那么把它放在新节点的位置。因为newCap变了,所以参与计算的掩码个数变了,自然结果也变了 52 if (e.next == null) 53 newTab[e.hash & (newCap - 1)] = e; 54 //如果老节点为红黑树,那么树进行剪枝 55 else if (e instanceof TreeNode) 56 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 57 // 58 else { // preserve order 59 Node<K,V> loHead = null, loTail = null; 60 Node<K,V> hiHead = null, hiTail = null; 61 Node<K,V> next; 62 do { 63 next = e.next; 64 //根剪枝的判断往哪部分分的方法一样,一部分是low, 详细说一下,如果这个表达式为0的话就,即原来就存在的桶的位置,因为原先参与计算的是(oldCap-1)&hash,即oldCap之后位的都没参加运算 65 if ((e.hash & oldCap) == 0) { 66 if (loTail == null) 67 loHead = e; 68 else 69 loTail.next = e; 70 loTail = e; 71 } 72 //这一部分是新的位置 73 else { 74 if (hiTail == null) 75 hiHead = e; 76 else 77 hiTail.next = e; 78 hiTail = e; 79 } 80 } while ((e = next) != null); 81 //把low的这部分保存原位置 82 if (loTail != null) { 83 loTail.next = null; 84 newTab[j] = loHead; 85 } 86 //把high这部分放入新位置 j+oldcap 87 if (hiTail != null) { 88 hiTail.next = null; 89 newTab[j + oldCap] = hiHead; 90 } 91 } 92 } 93 } 94 } 95 return newTab; 96 } 97
(4)Function put. 看一下最常用的方法。put
实际上是putVal。
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 }
(5) Function putVal.原来一个put方法这面复杂。
1 /** 2 * 传入参数是4个。hash,注意是Node的hash计算方法,然后是K,V,onlyIfAbsent(不存在时候才执行),evict(原注释写的,如果为false,那么Map为创建状态) 3 * 4 */ 5 6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 7 boolean evict) { 8 Node<K,V>[] tab; Node<K,V> p; int n, i; 9 //如果为老的table为空,则resize 10 if ((tab = table) == null || (n = tab.length) == 0) 11 n = (tab = resize()).length; 12 //找到桶的位置,如果为空的话 13 if ((p = tab[i = (n - 1) & hash]) == null) 14 //在该位置加入一个新的节点 15 tab[i] = newNode(hash, key, value, null); 16 //如果已经有节点存在了 17 else { 18 Node<K,V> e; K k; 19 //如果hash值和头节点的hash相等,并且key的地址和内容都相等,整个if,else就是找e的位置 20 if (p.hash == hash && 21 ((k = p.key) == key || (key != null && key.equals(k)))) 22 //e直接等于头节点 23 e = p; 24 //如果有一样不相等并且头节点是红黑树的话,则加入红黑树中 25 else if (p instanceof TreeNode) 26 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 27 //如果有一样不相等,并且该节点不属于红黑树的话 28 else { 29 //遍历这个链表,记录位置 30 for (int binCount = 0; ; ++binCount) { 31 //如果next为空 32 if ((e = p.next) == null) { 33 //直接加到链表的尾部 34 p.next = newNode(hash, key, value, null); 35 //如果链表太长了,则生成红黑树 36 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 37 treeifyBin(tab, hash); 38 break; 39 } 40 //遍历的过程也有可能遇到两个key相等,也就是找到了 41 if (e.hash == hash && 42 ((k = e.key) == key || (key != null && key.equals(k)))) 43 break; 44 p = e; 45 } 46 } 47 //如果e不为null 48 if (e != null) { // existing mapping for key 49 V oldValue = e.value; 50 //改变老的oldValue为新的value 51 if (!onlyIfAbsent || oldValue == null) 52 e.value = value; 53 afterNodeAccess(e); 54 return oldValue; 55 } 56 } 57 //如果结构改变了,即增加了元素,则modCount改变,用左fast fail机制 58 ++modCount; 59 //判断是否要resize 60 if (++size > threshold) 61 resize(); 62 afterNodeInsertion(evict); 63 return null; 64 }
(6)Function get.
1 /** 2 * 如果取不到则为null,具体实现在getNode中 3 */ 4 public V get(Object key) { 5 Node<K,V> e; 6 return (e = getNode(hash(key), key)) == null ? null : e.value; 7 }
(7)Function getNode
1 /** 2 * 参数两个,一个是hash方法后的hash值,另一个是key 3 */ 4 final Node<K,V> getNode(int hash, Object key) { 5 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 6 7 //如果table存在并且确实有映射时,并且位置(n-1) & hash 存在时 8 if ((tab = table) != null && (n = tab.length) > 0 && 9 (first = tab[(n - 1) & hash]) != null) { 10 //如果第一个节点的hash值和目标的哈希值相等并且是一个key时候,返回 11 if (first.hash == hash && // always check first node 12 ((k = first.key) == key || (key != null && key.equals(k)))) 13 return first; 14 //否则遍历链表 15 if ((e = first.next) != null) { 16 //如果是红黑树则遍历红黑树寻找 17 if (first instanceof TreeNode) 18 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 19 do { 20 //否则遍历链表查找 21 if (e.hash == hash && 22 ((k = e.key) == key || (key != null && key.equals(k)))) 23 return e; 24 } while ((e = e.next) != null); 25 } 26 } 27 return null; 28 }