最近在看HashMap底层源码,看了很多小有收获,下面是针对一些HashMap相关面试总结的内容:
1.HashMap与HashTable的区别?
相同点:
- HashMap和HashTable都是基于哈希表的数据结构
- HashMap和HashTable都实现了Map和Serializable接口
- 元素类型都是key-value形式
不同点:
- HashMap继承AbstratMap类;HashTable继承Dictionary类
- HashMap允许key-value为空,HashTable不允许键值为空
- HashMap进行put,get操作比HashTable效率高(HashTable对put,get加了同步锁)
- HashMap是线程不安全的(1.7数据丢失、数据覆盖、死循环,1.8解决了数据丢失和死循环问题但还是会出现数据覆盖);HashTable是线程安全的
- HashMap初始容量是16,扩容大小为2n;HashTable初始容量为11,扩容大小为2n+1
2.HashMap在JDK1.7与JDK1.8有什么区别?
- JDK1.7是数组+单链表的数据机构;JDK1.8是数组+单链表+红黑树的数据结构(当数组的size大于64时且链表深度大于8时由链表转为红黑树,提高查询效率)
- jdk1.7采用头插法和jdk1.8采用尾插法(避免了出现死循环问题)
- jdk1.7和jdk1.8扩容方式不同
3.HashMap数组的容量为什么为2的n次幂
Hash存取高效必须使尽量减少碰撞、使数据均匀分布在数组中;
put操作时,需要将key的hash值与table-1做与运算来找到桶的下标位置,经过计算当数组长度为2的次幂时,数据在数组上分布比较均匀,从而使get操作更高效。
(为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;
)
4.jdk1.8为什么要用红黑树,为什么不直接使用链表?
红黑树的时间复杂度是O(logn),链表的时间复杂度是O(n);
树的节点是链表节点的两倍,当节点较少时,树占用内存较大,读写效率和链表相差不多;当节点较多时,链表的效率很低此时内存劣势可以忽略,此时采用红黑树来提高效率
5.HashMap解决线程不安全问题为什么不用HashTable而采用concurrentHashMap?
HashTable使用同步锁解决线程不安全问题,多个线程竞争一把锁,效率太低
concurrentHashMap采用锁分段技术,将数据分段存储加锁,减少了锁的碰撞几率,效率高
6.HashMap在JDK1.7是怎样造成死循环的?
多个线程同时进行put操作时,容易造成死循环
假设当前的表为 3->7-null,长度为2,扩容条件是1,即再put一个元素就需要扩容
线程一:执行到(Entry<K,V> next = e.next; )挂起此时,e=key(3) e.next=7
线程二:执行第一次 e=key(3) e.next=key(7);第二次:执行e=key(7) next=null ,第三次e=null结束,全部执行完毕链表为7->3->null
线程一解封,上次执行到e.next=key(7),继续执行,e=key(7),next=key(3) ;此时链表死循环3-7-3
7.HashMap造成数据覆盖过程?
线程a线程b同时做put操作,插入同一下标(源码中put操作,插入数据之前判断是否发生了碰撞,若没有则直接插入),通过hash碰撞校验后被挂起;此时线程B进行插入操作插入数据,线程a解除挂起后继续操作时会覆盖线程b的数据。
HashMap的扩容过程
当hashmap超过了负载因子定义的容量时,也就是map的容量大于本身的75%时,会进行扩容。
第一步:扩容,创建一个新的数组,长度为原数组的两倍
第二步;rehash,遍历原数组,把数据重新hash到新的数组中(需要重新hash是因为,变为新数组后hash规则发生了变化)
hash公式:index = HashCode(Key) & (Length - 1)