说起HashMap,大家可能都不陌生,因为它的使用场景很广泛。
今天让我们来追寻一下HashMap(JDK1.8)的实现,我采用源码加注释来解读,还请耐下心看下去。
一、基本参数
//默认的初始容量为16,而且容量必须是2的幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量为2的30次方,如果你在构造方法中给了更大的值,则使用最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认负载因子0.75,为什么是0.75,是基于时间和空间的最佳值 static final float DEFAULT_LOAD_FACTOR = 0.75f; //转换为红黑树的链表长度的阈值,也就是当链表长度大于等于8时,将链表转换为红黑树 static final int TREEIFY_THRESHOLD = 8; //将红黑树转换为链表的阈值,也就说当红黑树节点小于等于6时,将红黑树转换为链表 static final int UNTREEIFY_THRESHOLD = 6; //呵呵,这个不知道是啥 static final int MIN_TREEIFY_CAPACITY = 64;
二、构成hash表的节点
static class Node<K,V> implements Map.Entry<K,V> { //这个节点的Hash值,用于计算节点在hash表中的位置 final int hash; //Map中的键 final K key; //Map中的值 V value; //基于链地址法解决哈希冲突的链表的下一个节点 HashMap.Node<K,V> next; //构造方法 Node(int hash, K key, V value, HashMap.Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } }三、成员变量
//存储元素的hash表 transient HashMap.Node<K,V>[] table; //包含每个entry的Set集合, transient Set<Map.Entry<K,V>> entrySet; //包含的元素个数 transient int size; //hash表的修改次数,这个计数只跟改变hash表结构的操作数相同 transient int modCount; //Map要进行扩容的阈值 int threshold; //哈希表的负载因子 final float loadFactor;
四、常用方法
1、构造方法
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
构造方法没什么说的,要注意的是构造函数没有可以和Hash表有关的东西,所以这个时候Hash表是null。这个之后会说。
2、hash方法
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hash方法,求Key的hash值,将key的hashCode值和右移16位的HashCode值进行异或。只是为了将Hash值更加均匀,举个例子,假如有hashCode值
10 1110 0011 1010 1110 1001 原hashCode值
10 1110 右移16位后的HashCode值
10 1110 0011 1010 1100 0111 得到的Hash值,就是为了让hashCode值的后16位和前16位都加入到hash算法中。
3、get方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
get方法很简单,知识简单的将key的hash值和key传给getNode方法,让getNode方法去判断。
4、getNode方法
final HashMap.Node<K,V> getNode(int hash, Object key) { //哈希表 HashMap.Node<K,V>[] tab; //first表示用hash值映射到哈希表中的那个元素,e是用来遍历可能存在的frist的后继节点 HashMap.Node<K,V> first, e; //哈希表的容量 int n; //用来表示first的key值 K k; //如果哈希表不为空 并且 哈希表长度大于0 并且根据传入hash值能找到相应的元素 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //如果这个节点的hash值和hash值相同,并且k==key或k.equalskey,则返回该元素 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 HashMap.TreeNode) return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key); //遍历链表 do { //如果这个节点的hash值和hash值相同,并且k==key或k.equalskey,则返回该元素 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //根据hash值找不到相应的元素 return null; }
如果你想自己了解源码,简易你将源码的某个方法拷到你的IDE,然后进行加注释理解,这样很有帮助。
5、containsKey方法
public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }
这个方法非常简单,相当于调用get方法,如果get方法返回值非空,则说明包含该键,否则不包含
6、put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
调用了putValue方法,传入键的hash值和键值对
7、putValue方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //哈希表 HashMap.Node<K,V>[] tab; //要插入的节点 HashMap.Node<K,V> p; //n为哈希表的长度 i是根据hash值计算出来的索引 int n, i; //如果哈希表为空 或者 哈希表的长度等于0 if ((tab = table) == null || (n = tab.length) == 0) //扩容哈希表 n = (tab = resize()).length; //根据hash值计算出索引i所处的的位置若为null,则说明那个位置没有元素,则直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //说明根据hash值计算出索引i所处的的位置有元素 else { //e为这个位置的原有元素 HashMap.Node<K,V> e; //k为这个位置的原有元素的键 K k; //如果要插入的元素的hash值,以及键与原有的元素的hash值和键相等,则进行替换 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果要插入的节点是树节点,则进行树节点的插入方法 else if (p instanceof HashMap.TreeNode) e = ((HashMap.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); //如果插入后链表长度大于8,则转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //后继节点的hash和键 与 插入节点的hash值以及键相同,则进行节点替换 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; //如果哈希表中的元素大于要扩容的阈值,则进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
要注意的是,在put的时候才会初始化哈希表。还有就是如果发生哈希冲突,是将节点插入到链表末尾,而不是像JDK1.7插入到链表头部。
8、resize方法
final HashMap.Node<K,V>[] resize() { HashMap.Node<K,V>[] oldTab = table; //获取哈希表的长度,两种情况,初次put时哈希表未初始化,长度为0,其他状况都是哈希表的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //原先的扩容阈值 int oldThr = threshold; //初始化新的哈希表容量和新的扩容阈值 int newCap, newThr = 0; //如果原哈希表长度大于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 } //如果原扩容阈值大于0,将哈希表的新容量设为原先的阈值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //这是初次初始化哈希表,将哈希表长度设为16,扩容阈值设为16*0.75 = 12 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"}) HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap]; table = newTab; //如果不是初次扩容,也就是哈希表已经初始化过了 if (oldTab != null) { //遍历原哈希表,进行再哈希 for (int j = 0; j < oldCap; ++j) { //遍历到的节点 HashMap.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 HashMap.TreeNode) ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap); //说明这是至少包含两个节点的一个链表 else { // preserve order //ok,这里定义了两条链表,高位链表和低位链表 HashMap.Node<K,V> loHead = null, loTail = null; HashMap.Node<K,V> hiHead = null, hiTail = null; HashMap.Node<K,V> next; do { next = e.next; //可以看出,低位链表保存原hash值与oldCap(不是oldCap-1)与运算为0的元素 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //高位链表保存原hash值与oldCap(不是oldCap-1)与运算不为0的元素 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; }可以看出,原哈希表中的单节点链表直接进行再哈希放入新的哈希表,红黑树采用红黑树的方法,而多节点链表分为高位和低位链表。如果将多节点链表进行再哈希,此时可能会造成额外的时间和空间上的开销。
9、remove方法
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
remove方法直接调用removeNode方法来进行节点的删除
10、remove方法
final HashMap.Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, index; //根据传入的hash值可以在哈希表中找到相应的元素 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { //node表示当前要删除的节点,而e用来遍历链表每个节点 HashMap.Node<K,V> node = null, e; K k; V v; //如果根据hash值和key找到了相应的元素,且元素在哈希表链表的头部 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { //如果是树节点,则在树中找该节点 if (p instanceof HashMap.TreeNode) node = ((HashMap.TreeNode<K,V>)p).getTreeNode(hash, key); else { //在链表中寻找该节点 do { //如果找到了节点,则执行下一步,也就是删除 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //如果找到的节点不为空 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //如果是树节点,则进行树节点的删除 if (node instanceof HashMap.TreeNode) ((HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //找到了,并且该元素是单节点链表 else if (node == p) tab[index] = node.next; //找到了,是多节点链表 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
最后总结一下:
1、JDK1.8的hashMap采用数组+链表+红黑树实现的
2、为什么转换红黑树的阈值设为8,而转换链表的阈值设为6?