Java中HashMap源码分析
- 2019 年 10 月 4 日
- 笔记
JDK的1.6,1.7版本中,HashMap使用数组+链表来实现的,通过计算Map中的key的的hash值来确定该key在数组中index的位置。计算key在数组中位置,使用的是hash算法,HashMap中定位到桶的位置 是根据Key的hash值与数组的长度取模来计算的。取模可以改为:hashCode & (length – 1)。看下JDK8中的hash 算法:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
在此版本中,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
在JDK1.8中,HashMap使用的是数组+链表+红黑树实现。当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
HashMap是基于hashing的原理,我们使用put(key, value)
存储对象到HashMap中,使用get(key)
从HashMap中获取对象。当我们给put()
方法传递键和值时,先对Key调用hashCode
方法,来计算hash值,返回的hash值用来找bucket对象,来放entry键值对。
HashMap创建的时候,会创建一个Entry数组,HashMap是有entry数组组成的,每个Entry对象都是null。每个Entry存放的有hash,key,value,next。
HashMap中最主要的两个方法:get(key)
和put(key, value)
;
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //该hash值所在的元素是保存在table[(tab.length-1)&hash]这个位置,所以需要确保在该位置上有Node 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; }
首先,如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null。
get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可。
put(key, value)
方法:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ 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) // -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; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
1,判断键值对数组tab[]是否为空或为null,否则以默认大小resize();2,根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入33,判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理红黑树源码:
//红黑树 static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> { TreeNode<k,v> parent; // 父节点 TreeNode<k,v> left; //左子树 TreeNode<k,v> right;//右子树 TreeNode<k,v> prev; // needed to unlink next upon deletion boolean red; //颜色属性 TreeNode(int hash, K key, V val, Node<k,v> next) { super(hash, key, val, next); } //返回当前节点的根节点 final TreeNode<k,v> root() { for (TreeNode<k,v> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } }
HashMap 各常量、成员变量作用
// 创建 HashMap 时未指定初始容量情况下的默认容量 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 3 4 // HashMap 的最大容量 5 static final int MAXIMUM_CAPACITY = 1 << 30; 6 7 // HashMap 默认的装载因子,当 HashMap 中元素数量超过 容量*装载因子 时,进行 resize()操作 8 static final float DEFAULT_LOAD_FACTOR = 0.75f; 9 10 // 用来确定何时将解决 hash 冲突的链表转变为红黑树 11 static final int TREEIFY_THRESHOLD = 8; 12 13 // 用来确定何时将解决 hash 冲突的红黑树转变为链表 14 static final int UNTREEIFY_THRESHOLD = 6; 15 16 /* 当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量, 若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )导致的 hash冲突太多,则不进行链表转变为红黑树操作,转为利用 resize() 函数对 hashMap 扩容 */ 17 static final int MIN_TREEIFY_CAPACITY = 64;
加载因子(默认0.75):map中值如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。
1 //保存Node<K,V>节点的数组 2 transient Node<K,V>[] table; 3 4 //由 hashMap 中 Node<K,V> 节点构成的 set 5 transient Set<Map.Entry<K,V>> entrySet; 6 7 //记录 hashMap 当前存储的元素的数量 8 transient int size; 9 10 //记录 hashMap 发生结构性变化的次数(注意 value 的覆盖不属于结构性变化) 11 transient int modCount; 12 13 //threshold的值应等于 table.length * loadFactor, size 超过这个值时进行 resize()扩容 14 int threshold; 15 16 //记录 hashMap 装载因子 17 final float loadFactor;
HasMap的扩容机制resize()源码:
构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时。
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ 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; } /*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) /*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/ newThr = oldThr << 1; // double threshold } /*如果旧表的长度的是0,就是说第一次初始化表*/ 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;//把新表赋值给table 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)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); /*如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重*/ 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;//记录下一个结点 //新表是旧表的两倍容量,实例上就把单链表拆分为两队, //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对 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) {//lo队不为null,放在新表原位置 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置 hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
在java jdk8中对HashMap的源码进行了优化,在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)。
你可能还知道哈希碰撞会对hashMap的性能带来灾难性的影响。如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。
随着HashMap的大小的增长,get()方法的开销也越来越大。由于所有的记录都在同一个桶里的超长链表内,平均查询一条记录就需要遍历一半的列表。
JDK1.8HashMap的红黑树是这样解决的:
如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。
关于HashMap的总结:
1.当两个对象的hashcode相同会发生什么?
HashMap底层是由数组以及键值对组成的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来确定存储位置的,HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。
哈希表是由数组+链表组成的,因为hashcode
相同,所以它们的bucket
位置相同,‘碰撞’会发生。因为HashMap
使用LinkedList
存储对象,这个Entry会存储在LinkedList
中,这个存储的位置一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
2.如果两个键的hashcode相同,你如何获取值对象?
HashMap会使用键对象的hashcode
找到bucket位置,找到bucket
位置之后,会调用keys.equals()
方法去找到LinkedList中正确的节点,最终找到要找的值对象。从HashMap中get元素时,首先计算key的hashCode
,找到数组中对应位置的某一元素,然后通过key的equals
方法在对应位置的链表中找到需要的元素。
3.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing
,因为它调用hash方法找到新的bucket位置。
4.重新调整HashMap大小存在什么问题?
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。