HashMap是Java编程中最经常使用的数据结构之一,本文基于JDK1.8从源码角度来分析HashMap的存储结构和经常使用操做。HashMap实现了Map接口,Map接口的实现类还有Hashtable、LinkedListHashMap和TreeMap。具体的继承结构请参考JDK Document。html
学过数据结构的同窗都知道Hash表的实现方式,其实HashMap就是Hash表的一个实现。HashMap是key-value结构的,根据key的hashCode能够快速访问到key对应的value,访问操做的时间复杂度为O(1)。但HashMap在多线程的场景下并不能保证数据的一致性,若是要在多线程的场景下使用Map结构,能够考虑使用Collections工具类的synchronizedMap方法使HashMap变为线程安全的,同时也能够考虑使用ConcurrentHashMap。java
那HashMap和其余几个Map接口的实现类有什么区别呢?node
和Hashtable的区别:Hashtable是线程安全的,是JDK的遗留类,内部实现使用synchronized关键字对方法加锁,效率和并发性很差。在线程安全的场景下可使用ConcurrentHashMap替代,ConcurrentHashMap内部实现使用了分段锁,效率和并发性都要比Hashtable好。另外一个区别是HashMap能够有有个null键和多个null值,Hashtable是不能够的。编程
和LinkedHashMap的区别:LinkedHashMap是Map的实现类同时也是HashMap子类,与HashMap不一样的地方在于LinkedHashMap底层使用链表实现,所以LinkedHashMap可以维护记录插入顺序,可以按次序访问,而HashMap的key是无序的,这一点和HashSet一致。api
和TreeMap的区别:TreeMap实现了Map的同时也实现了SortedMap接口,底层基于RB-Tree(红黑树)实现,TreeMap可以根据天然序或者给定的比较器维护记录的存储顺序。须要注意的是,在使用TreeMap的时候key对象须要实现Comparable接口或者在构造TreeMap时传入自定义Comparator,不然会在运行时抛出java.lang.ClassCastException异常。数组
在使用Map时,须要确保key对象是不可变的,也就是说key的hash是不会改变的,若是key的hash发生变化,就会出现key访问不到value的状况。须要保证equals()方法和hashCode()方法所描述的对象是一致的,即两个对象的equals()方法返回true那么这两个对象的hashCode()方法也要返回相同的值。这也是重写equals()方法一般也要重写hashCode()方法的缘由。安全
HashMap的结构是数组、链表和RB-Tree的组合,整体来讲是数组用来进行hash寻址,用链表存储hash冲突的Entry,在冲突多时用RB-Tree来提升存取效率。数据结构
在HashMap的结构中存储的是key-value实体Entry<K,V>,更准确的说是存储的Node<K,V>,Node<K,V>是HashMap的一个静态内部类,实现了Map.Entry接口。是key-value的包装类。多线程
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) {...} 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) {...} 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中有一个Node[]类型的字段,用来当作hash桶,Node中hash字段用来快速定位hash桶的索引。并发
transient Node<K,V>[] table; //(transient关键字做用是在序列化时过滤掉此字段)
除此以外,HashMap还有几个比较重要的字段。
//HashMap中全部key-value实体的集合 transient Set<Map.Entry<K,V>> entrySet; //当前HashMap的大小(k-v实体个数) transient int size; //整个HashMap结构变化的次数 transient int modCount; //在下次扩容以前能容纳k-v实体的最大值,threshold=(capacity * load factor)。 int threshold; //负载因子 final float loadFactor;
HashMap的初始化时把HashMap所须要的数据结构和字段构造出来,并给定初始字段值。好比构造Node数组,设定初始化容量和负载因子等。这些能够经过HashMap的构造方法来实现。若是构造HashMap时不指定initialCapacity和loadFactor就会使用默认值,initialCapacity的默认值是16,HashMap的最大容量是2^30;默认的loadFactor值为0.75,含义是在存储数量达到当前Node[]数组长度的75%时进行下一次扩容。默认0.75也是hash冲突和空间利用率之间的权衡。
注意,loadFactor的值是能够大于1的,由于threshold=capacity * load factor,这里的capacity是Node[]数组的长度,除Node[]数组外使用链表和红黑树来存储冲突的记录,因此理论上整个HashMap对象存储的记录数能够大于capacity,也就是说size并不被capacity所限制。
当HashMap存储的记录数达到threshold=capacity * load factor后就要进行一次扩容,把容量扩大到以前的2倍,具体方法使建立一个新的长度为原来2倍的Node[]数组替换掉以前的Node[]数组。替换数组并非简单的拷贝而是要把记录分散在新的数组中。在JDK1.8之前是采用rehash的方法,JDK1.8对此作了优化,避免了从新计算hash并且能将记录均匀的分散在新的Node[]数组中。具体作法是,在Node[]数组扩容到原来的2倍时,key的hash长度在原来的基础上多出一位,那么这一位能够是0也能够是1,当是0时索引不变,1时索引变为原索引+原容量。由于0和1是能够认为是随机的因此均匀分布的效果和rehash理论上是一致的。
来欣赏一下JDK1.8优化后的resize代码,简直是艺术品。
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) { //超过2^30就不能再扩容了,把threshold设置为int最大值,就不会再扩容。 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //没有超过最大值就扩容到原先的2倍 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 //第一次初始化没有指定容量,使用默认容量16 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { //计算新的threshold 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; //若是新增高位为0,索引位置不变 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //若是新增高位为1,索引位置变为原索引+oldCap 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; } //放置原索引+oldCap位置 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
弄清楚了HashMap的结构和扩容机制,put()和get()操做直接按照步骤来分析就能够了。put()操做的主要是以下几个步骤:
public V put(K key, V value) { //传入key的hash值,对hashCode值作位运算 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; //若是tab为null,则经过resize初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //计算key的索引,若是为当前位置为null,直接赋值 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //若是当前位置不为null 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) // -1 for 1st treeifyBin(tab, hash); break; } //找到相同节点则覆盖 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //覆盖 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //结构变化次数+1 ++modCount; //若是size超过最大限制,扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
明确了put()方法,get()方法的分析就变得很是容易了,首先看一下如何经过hash肯定key在桶中的索引位置。
static final int hash(Object key) { //jdk1.8 & jdk1.7 int h; // h = key.hashCode() 为第一步 取hashCode值 // h ^ (h >>> 16) 为第二步 高位参与运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //jdk1.8已经把这个方法省略了,可是在访问时直接使用这个计算策略。 static int indexFor(int h, int length) { return h & (length-1); //第三步 取模运算 }
以下就是get()方法的具体分析:
public V get(Object key) { Node<K,V> e; //传入key的hash 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; //这里访问(n - 1) & hash其实就是jdk1.7中indexFor方法的做用 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //判断桶索引位置的节点是否是相同(经过hash和equals判断),若是相同返回此节点 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); } } //若是不存在返回null return null; }
除以上的分析之外,HashMap还有许多其余方法,包括判空、删除、清空、替换、遍历以及JDK1.8新增的函数式语法和Lambda表达式的内容。代码总行数多达两千多行,若是感兴趣或遇到相应问题能够具体分析。已经了解了HashMap的存储结构和关键操做的步骤,再去分析其余方法就比较容易了。
从以上的对HashMap源码的分析,能够得出一些使用上的技巧和有用的结论。
参考资料:
Java™ Platform, Standard Edition 8 API Specification
java.util.HashMap源码