HashMap常见问题详解(put,扩容等)

一、HashMap的插入过程

1.判断数组是否为空,为空进行初始化。
2.不为空,计算key的哈希值,然后通过(hash&(数组长度 -1))计算出key在数组中的下标index。
3.查看table[index]是否为空,为空就利用传入的key和value构造一个新的Node节点存入table[index]
4.table[index]不为空,说明发生了哈希冲突,然后查看key是否相同,相同就用新的value替换掉原来的值。
5.如果key不相同,然后判断当前节点是不是树形节点,是的话查找树中是否存在元素的key与传入的key相等,相等就用新的value值替换,不存在就按照红黑树的规则将封装后的node节点加入红黑树,然后经过变色,左旋,右旋等一系列操作使该数重新符合红黑树的规则。
6.如果当前节点不是树形节点,那就是链表,遍历这个链表,看是否存在元素的key与传入的key相等,相等就用新的value值替换,不存在就将该元素加入到链表尾部。判断链表长度是否大于8,大于八转为红黑树。
7.插入完成后总结点数加1,判断节点数是否大于扩容阀值,大于就就行扩容。

二、HashMap的扩容过程

oldCap:原来数组长度,oldThr:原来的扩容阀值
newCap:扩容后的数组长度 new Thr:下次进行扩容的阀值。

1.创建新数组

1.计算数组的长度为oldCap,oldCap > 0说明散列表已经初始化过了,newCap和newThr都变为原来的两倍。
2.oldCap=0且oldThr>0说明是采用有参构造new的HashMap,这时候只需要newCap = oldThr。
3.oldCap = 0且oldThr = 0说明采用的是无参构造new的HashMap,newCap = 16;NewThr = 16*0.75 = 12;
4.创建一个Node型的新数组newTable,长度为newCap。

2.数据迁移

5.遍历原来的数组,判断元素 e =oldTable[j]是不是null,不为null,判断e.next == null,不是null,说明桶中只有一个元素,newtable[e.hash&(newCap-1)=e。
6.e.next不为null且是树节点,则调用相关方法进行数据迁移。

三、HashMap的负载因子为啥是0.75

负载因子:负载因子是HashMap中的一个常量,默认是0.75,是用来计算扩容阀值的重要参数。
原因分析:我们都知道扩容阀值= 负载因子*数组长度。
1.当这个负载因子较小时,比如0.5,那么当哈希表中的元素个数等于数组长度的一半时就要进行扩容,这时候哈希表中的数据较少,进行增删改查的效率高,发生hash冲突的概率,但每次元素个数达到数组长度一半时就要进行扩容,造成了很大的空间浪费。
2.当这个负载因子比较大时,比如1,那么当哈希表中的元素个数等于数组长度的时候再进行扩容,虽然空间利用率提高了,但是当hash表中元素慢慢变得很多时,hash冲突发生的概率肯定会增加,并且链表长度和红黑树高度肯定会变大,增删改查效率低。
总结:负载因子的大小是一种时间和空间方面的权衡,需要综合考虑,这个0.75是大量数据测试出来的结果。

四、HashMap中在计算key的hash值时为什么不知用他的hashcode的值而要和key.hashCode做高十六位异或运算

static final int hash(Object key){
	int h;
	return(key == null) ? 0 : (h = key.hashCode())^(h >>> 16);
}

我们都知道hashmap中计算元素在散列表中的位置是通过hash&(数组长度-1)来计算的。
个人理解:key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为-2147283648~~2147483647,这个数很大,但是我们平时使用hashmap过程中,数组的长度不会特别大,而&运算的特点就是有0则0,所以key.hashcode这个结果的高位根本不会用到,每次都是地位参加运算,虽然key.hashCode这个数范围很大,随机性强,但是每次只有低位参与运算,高位不影响结果,那么随机性就降低了,哈希冲突更容易了,所以我们就想办法让key.hashCode的高位也参与运算。所以就有了key.hashCode右移16位再和key.hashCode做异或运算。
总结
key.hashCode右移16位再和key.hashCode做异或运算,key.hashCode的高位和低位就被混合起来,低位的随机性就会大大增强,并且混合后的低位参杂了高位的部分特征,这样高位的信息也被变相的保留下来

五、HashMap中的数组长度为什么一定要是2的整次幂的数

源码中有参创建hashmap时对传入的数组长度做的处理

static final int tableSizeFor(int cap){
//该方法就是把任意一个数字变为一个2的整次幂的数。
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中计算索引的方法是hash&(length - 1),我们知道hashmap中发生哈希冲突对效率会产生很大的影响,所以无论做什么,一定要尽量避免哈希冲突的发生。
所以哦我们要保证hash&(length - 1)的随机性强,假设length - 1 = index,&运算符的特点是有0则0,那么假如index = 0b1001,那么对于hash来说,中间两位是什么已经不影响最终的结果了,hash&index结果就变为只有四种情况。所以我们发现,index这个数中0的出现对结果影响很大,所以0不能出现在index的二进制编码里面,意思就是要全部为1,二进制编码全为1的数加1一定是一个2的整次幂。这也就是为啥数组长度在与hash做运算的时候要减1的原因

七、jdk1.7和1.8中hashmap有什么不同

1.jdk1.7是数组+链表的结构,jdk1.8中是数组+链表+红黑树的结构
2.jdk1.7链表中数据插入的方式是头插法,插入元素要放到桶中,原来元素作为插入元素的后继元素。而jdk1.8采用的是尾插法,直接放到链表尾部。
3.jdk1.7在扩容的时需要对元素进行重新哈希以确定元素在新数组中的位置,而jdk1.8中不需要重新哈希,要么存储在和原数组相同的位置,要么存储在原数组位置+原数组长度的位置。

//jdk1.7中扩容的核心
void transfer(Entry[] newTable ,boolean rehash){
	int newCapacity = newTable.length;
	for(Entry<k,v> e : table){
		while(null != e){
			Entry<k,v> next = e.next;
			if(rehash){
				e.hash = null == e.key ? 0 : hash(e.key);
			}
			//重新hash来确定在新数组中的存储位置
			int i = indexFor(e.hash,newCapacity);
			e.next = newTable[i];
			e = next;
		}
	}
}
static int indexFor(int h, int length){
	return h&(length - 1);
}

4.jdk1.7中是先判断是否需要扩容,再插入,而jdk1.8是先插入,在扩容
改进后的优点
1.减少哈希冲突,提高操作效率。
2.因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环。

八、扩容的时候为什么1.8不用重新hash就可以直接定位到源节点再新数组的位置

这是因为扩容的时候数组变为原来的2倍,用于计算数组位置的掩码仅仅是比高位多了个1,比如扩容前数组长度为16,(n - 1)&hash中 n-1 = 0b 0000 1111,扩容后的长度是32,此时n-1 = 0b 0001 1111。&运算符的特点是有0则0,所以对于扩容前(n - 1)&e.hash和扩容后(n - 1)&e.hash的结果就只有两种情况,hash的倒数第五位是0还是1,如果是0,那么扩容后元素在新数组的位置与扩容前元素在新数组中的位置相同。如果为1,扩容后元素在新数组中的位置=扩容前元素在旧数组中的位置+旧数组的长度,所以,我们就不需要重新计算元素的位置,判断一下就可以了,jdk1.8采用的**(e.hash)&原数组长度**的结果来判断的。这种方式非常巧妙,原数组的长度的二进制编码的首位和扩容后数组长度对应的位置上的数是相同的,结果为0,以上面的例子来说,e.hash = ob xxx0 xxxx,扩容前后位置一样。结果不为0,e.hash = ob xxx1 xxxx,扩容后的位置= 扩容前位置+原数组长度。

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;
    }
 }

九、HashMap和HashTable的区别

1.hashmap是线程不安全的,hashtable是线程安全的每个方法都加了synchronized。
2.hashmap的效率高于hashtable,因为hashtable加了synchronized会发生阻塞,严重影响效率。
3.hashmap中允许一个key为空,值多个为空,hashtable不允许有。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值