hashMap是面试中的高频考点,或许平常工做中咱们只需把hashMap给new出来,调用put和get方法就完了。可是hashMap给咱们提供了一个绝佳的范例,展现了编程中对数据结构和算法的应用,例如位运算、hash,数组,链表、红黑树等,学习hashMap绝对是有好处的。
废话很少说,要想学习hashMap,必先明白其数据结构。在java中,最基础的数据结构就两种,一种是数组,另一个就是模拟指针(引用),一块儿来看下hashMap结构图:java
从类定义上看,继承于AbstractMap,并实现Map接口,其实就是里面定义了一些经常使用方法好比size(),isEmpty(),containsKey(),get(),put()等等,Cloneable,Serializable 的做用在以前list章节已讲述过就再也不重复了,总体来讲类定义仍是蛮简单的node
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
接下来会带领你们阅读源码,有些不重要的,会咔掉一部分。面试
//初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默认加载因子,用来计算threshold static final float DEFAULT_LOAD_FACTOR = 0.75f; //链表转成树的阈值,当桶中链表长度大于8时转成树 static final int TREEIFY_THRESHOLD = 8; //进行resize操做室,若桶中数量少于6则从树转成链表 static final int UNTREEIFY_THRESHOLD = 6; //当桶中的bin树化的时候,最小hashtable容量,最少是TREEIFY_THRESHOLD 的4倍 static final int MIN_TREEIFY_CAPACITY = 64;
//在树化以前,桶中的单个bin都是node,实现了Entry接口 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 int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } }
//jdk1.8 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hashMap中的hash算法,影响hashMap效率的重要因素之一就是hash算法的好坏。hash算法的好坏,咱们能够简单的经过两个因素判断,1是否高效2是否均匀。
你们都知道key.hashCode调用的是key键值类型自带的哈希函数,返回int散列值。int值得位数有2的32次方,若是直接拿散列值做为下标访问hashMap主数组的话,只要hash算法比较均匀,通常是很难出现碰撞的。可是内存装不下这么大的数组,因此计算数组下标就采起了一种折中的办法,就是将hash()获得的散列值与数组长度作一个与操做。以下函数:算法
//或许你们会发现这个方法是jdk1.7,为何不用1.8的呢?那是由于1.8里已经去掉这个函数,直接调用,为了讲解方便,我从1.7中找出此方法方便学习 static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
hashMap的长度必须是2的幂次方,最小为16.顺便说一下这样设计的好处。由于这样(length-1)正好是一个高位掩码,&运算会将高位置0,只保留低位数字。咱们来举个例子,假设长度为16,16-1=15,15的2进制表示为
00000000 00000000 00000000 00001111.随意定义一个散列值作&运算,结果如所示:编程
10101010 11110011 00111010 01011010 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00001010
也就是说实际上只截取了最低的四位,也就是咱们计算的索引结果。可是只取后几位的话,就算散列值分布再均匀,hash碰撞也会很严重,若是hashcode函数自己很差,分布上成等差数列的漏洞,使最后几个低位成规律性重复,这就无比蛋疼了。这时候hash()函数的价值就体现出来了数组
h=key.hashcode() 11111011 01011111 00011100 01011011 h>>>16 ^ 00000000 00000000 11111011 01011111 ------------------------------------- 11111011 01011111 11100111 00000100 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00000100
(h = key.hashCode()) ^ (h >>> 16),16正好是32的一半,其目的是为了将本身的高半区和低半区作异或,混合高低位信息,以此来加大低位的随机性,并且混合后的低位存在部分高位的特征,算是变相的保留了高位信息。由此看来jdk1.8对于hash算法和计算索引值的设计就基本暴露在咱们的眼前了,不得不佩服设计之巧妙。数据结构
//返回大于等于cap且距离最近的一个2的幂 //例子:cap=2 return 4; cap=9 return 16; 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; }
//hashMap数组,保留着链表的头结点或者红黑树的跟结点。 //当第一次使用的时候,会对其初始化,当须要扩容时,会调用resize方法。长度必定是2的幂 transient Node<K,V>[] table; //用来遍历的set集合,速度快于keySet transient Set<Map.Entry<K,V>> entrySet; transient int size; //用来检测使用iterator期间结构是否发生变化,变化则触发fail-fast机制 transient int modCount; //当容器内映射数量达到时,发生resize操做(threshold=capacity * load factor) int threshold; //加载因子,默认0.75 final float loadFactor;
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 && // always check first node ((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; }
hash函数以前已经研究过了,直接锁定getNode()吧。经过hash函数算出hash值&上数组长度从而计算出索引值,而后遍历比较key,返回对应值。并发
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; //若table为空,则调用resize()进行初始化,并将长度赋值给n if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根据(n-1)&hash算出索引,获得结点p,若p为null,则生成一个新的结点插入。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //若p不为null,则将p和插入结点的key与其hash值进行比较,若相同将p的引用同时赋值给e Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若不一样,且结点p属于树节点,则调用putTreeVal() else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //若不一样,则将p当作链表的头结点,循环比较,若为null则新增节点,且循环次数大于等于TREEIFY_THRESHOLD - 1则从链表结构转为树结构 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //若链表中其中一结点的key与hash与插入结点一致,则跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //若是插入的key存在,则替换旧值并返回 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //若插入的key在map中不存在,则判断size>thresold if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } //初始化数组和扩容使用 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; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults 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 { // preserve order 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; }
hashMap中遍历的方式是经过entrySet和keySet,为了保证其效率,建议用entrySet由于他的存储结构和hashMap一致。hashMap是如何维护entrySet的呢?经过阅读源码,发如今put的时候,并无对entrySet进行维护,且源码中
entrySet方法只是new了个对象,那这个entrySet视图的数据从哪而来呢?app
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } 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(); } } final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } } abstract class HashIterator { Node<K,V> next; // next entry to return Node<K,V> current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } }
经过阅读EntrySet,咱们发现其iterator() 调用了EntryIterator(),而在对其进行实例化的时候会对其父类HashIterator进行实例化,从HashIterator的构造方法和nextNode咱们发现,其返回的视图就是做用于table的,因此无需从新开辟内存。数据结构和算法
本篇文章主要分析hashMap的存储结构,分析了hashMap为何容量始终是2的幂,分析了其hash算法的好坏和影响其效率的因素,同时也了解到了在put和get时作了哪些操做和其中数据结构的变化。最后经过hashMap常见的遍历方式,得出entrySet是便利效率最高的,且hashMap维护entrySet的方式。经过学习,发现hashMap的设计很是优秀,但无奈能力有限,没法将其精妙之处所有剖析开来。下节预告:分析一下并发下的hashMap有可能形成的闭环问题和concurrentHashMap