HashMap是平时开发中使用最多的一个集合类之一,可以算作一个源码必读类,通过读源码可以了解其中内部原理,帮助我们去更好的使用。另外在实现上可以看得出大神们追求性能,子类与父类之间关系的精巧设计,有很多地方可以在日常开发中借鉴。、
HashMap使用了哈希表的数据结构来实现,简单来说一个Node数组。下面主要整理常用方法put,get,remove,以及补充与java7那些地方做了改进。
put方法:
public V put(K key, V value) {
//调用内部的一个方法,注意该方法是一个final类,子类不可覆盖,
//具体的实现逻辑都在这个方法里了,至于为什么要单独抽成一个方法,可以看下put和putIfAbsent这两个方法,
//他们调用的都是putVal方法,但是传入的onlyIfAbsent参数不一样,这个参数是用来判断是否需要覆盖原值
}
//
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//声明一个哈希表
HashMap.Node[] tab;
int n;
//如果当前哈希表为空时,进行初始化哈希表,如果创建HashMap对象时没有指定大小,那么默认大小为16
if ((tab = this.table) == null || (n = tab.length) == 0) {
n = (tab = this.resize()).length;
}
//接下来会先开始根据hash去取找对应的所在哈希表的位置,公式:i = hash & (n-1)
Object p;
int i;
//如果在哈希表中对应的数组角标下没有节点(Node),则直接创建新节点放在该角标下,返回null,modCount自增1,结束
if ((p = tab[i = n - 1 & hash]) == null) {
tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
} else {
Object e;
Object k;
//如果当前角标下已存在链表,则先判断头节点hash和待插入的hash是否一致,
//再判断key是否相等或者key对象覆写的equals方法是否返回true
if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
e = p;
} else if (p instanceof HashMap.TreeNode) {
//如果上面的与头节点对比结果是不一致,判断该头节点是否为红黑树节点,如果是则调用putTreeVal方法进入红黑树的插入逻辑
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
} else {
//如果该头节点是链表,那么则开始遍历链表,
int binCount = 0;
while(true) {
//如果链表中没有相同的key和key.equals()都返回false,那就在链表尾部进行插入新节点
//当插入节点后,当前链表长度大于等于8,并且此时哈希表长度大于等于64时,才会进行链表转化为红黑树,、
if ((e = ((HashMap.Node)p).next) == null) {
((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
if (binCount >= 7) {
//若仅仅链表长度大于等于8,但是哈希表长度小于64时,
//链表不会发生红黑树转换,而是会进行扩容,进行一次再散列
this.treeifyBin(tab, hash);
}
break;
}
//如果该链表下存在相同的key,则跳出循环
if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
break;
}
p = e;
++binCount;
}
}
//这里是一个是否覆盖原值的操作,如果put的key在原HashMap已经存在了,那么onlyIfAbsent为false时则会覆盖原来的value
//另外如果原来的value为null时,也会进行值的覆盖
if (e != null) {
V oldValue = ((HashMap.Node)e).value;
if (!onlyIfAbsent || oldValue == null) {
((HashMap.Node)e).value = value;
}
//这是一个访问节点之后的钩子方法,可以交给子类去覆盖,linkedHashMap的实现是将修改的节点放到维护的顺序链表的队尾
this.afterNodeAccess((HashMap.Node)e);
return oldValue;
}
}
//修改次数自增1
++this.modCount;
//容器size自增1,如果当前size大于扩容阈值。则进行扩容
if (++this.size > this.threshold) {
this.resize();
}
//这是一个添加节点之后的钩子方法,HashMap中没有具体逻辑,LinkedHashMap覆盖了这个方法,
//是一个删除最久远节点的方法,默认没有使用
this.afterNodeInsertion(evict);
return null;
}
下面是流程图
resize方法:
final Node<K,V>[] resize() { //声明一个变量指针指向HashMap中的node[],哈希表 Node<K,V>[] oldTab = table; //原哈希表长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //原扩容阈值,如果当前size大于扩容阈值时则会发生扩容 int oldThr = threshold; //声明新哈希表长度以及新的扩容阈值 int newCap, newThr = 0; //下面的逻辑比较绕,描述的时每次扩容大小以及再散列 if (oldCap > 0) {//这里判断当前哈希表长度已经到达最大值(2^31-1)不在进行扩容,并且将扩容阈值设置为最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //将新哈希表长度变为原来的两倍,如果原哈希表长度大于等于16,那么扩容阈值也变为原来的两倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //这里是用来初始化哈希表长度,可以仔细看一下HashMap的构造方法,创建HashMap对象不会立即去初始化哈希表, // 而是先初始化负载因子和扩容阈值(默认16),初始化容器在resize中完成,这里可能也是设计者为了考虑空间利用吧, // 等到需要用的时候再去初始化容器 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults //如果使用的是new HashMap(0)构造方法创建HashMap时,则这里使用默认的配置,初始容量为16,扩容阈值为12 //负载因子不得传0,否则会报错,可以看下三个构造函数,挺重要的 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //这里是用来处理当 (1)原哈希表长度大于0且小于16 或者 (2)容器进行两倍扩容时新哈希表长度大于等于2^31-1 这两种情况时 //对应上面的两种情况是 : 新的扩容阈值是newThr为 (1)新容器长度*负载因子(newCap * loadFactor) (2)2^31-1 //注意不是 HashMap非得当前size一旦达到当前哈希表长度*负载因子(oldCap * loadFactor)就会发生扩容,如果初始化HashMap哈希表长度小于16时, //会根据 length*loadfactor 进行扩容,如果初始化的哈希表长度为16或者16以上时,是根据当前size > length进行扩容, // 相当于一倍的哈希表长度 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; //下面就是再散列的逻辑了 //主要需要关注是遍历每一个链表如何将每一个节点重新找到在新哈希表的位置 //再散列时使用的计算公式时 hash & oldCap ,而之前put操作时使用的是 hash & (oldCap - 1) //这里精妙之处是因为HashMap的长度用于是2的倍数,此时二进制数是1后面带着n个0,那么再进行-1操作时,二进制树的位数会减一,但是所有位数就全部都是1, //将原容器进行两倍扩容等到的值再进行-1时,其二进制数还是一堆1,但是和原容器长度-1的二进制数相比,新的容器长度的会比原来在高位上多一个1 //所以此时只需要拿着hash原容器长度做一次与运算得到的结果时0还是1时,如果是0说明与最高位的那个1与结果是0, // 如果等于1说明这个hash与新的容器做hash & (newCap - 1)时,结果会hash & (oldCap - 1)多oldCap,可以自己推理下, // 在这里就可以算出拿到该节点在新哈希表下的角标时 原角标 + oldCap //那么为什么要这么做? //这样我认为能提高性能,节点hash与运算类似于10000000这种二进制数会比11111111速度更快些,因为这样算出来的结果只有0和1两种值 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 //具体再散列逻辑,先声明两个链表,一个是高位链表,一个是低位链表 //高位链表: hiHead,hiTail,节点 hash & oldCap == 1,新的节点位置 原index+oldCap //低位链表:loHead,loTail,节点 hash & oldCap == 0 新的节点位置 还是原位置 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; }
下面是流程图
get方法:
remove方法: