详细源码解析查看面试再也不怕问到HashMap(一)
HashMap干货总结
1,可以用null做为key和value
2,capacity和load factor影响性能
3,线程不安全
4,HashMap不能保证随着时间的推移Map中的元素次序是不变的。
5,迭代时并发更改了map,则会抛出ConcurrentModificationException
6,当同一个bucket上元素拥挤(默认值为8)时,会将列表转变成红黑树来提高查询性能,缩减时(默认值为6)会恢复为链表。红黑树查找位置时折半查找效率高于链表,而删除操作效率低于链表。
7,hash(key)核心hash算法,在长度为2的幂前提下,hash值高位下移16位,参与取模,降低碰撞,另外取模部分利用2的幂减1来做&操作提高了性能。
8,链表状态下碰撞元素加入时是放入链表尾部,因为它认为最近放入的元素可能最容易被使用。
9,继承Serializable接口,可是字段使用transient修饰,比如table,entrySet。原因是hashcode操作依赖jvm所处的环境因素,不同环境可能有不同的hash值,做成存储的内容既是序列化也无法通用.所以hashmap自己实现了writeObject和readObject,这里就需要知道java在序列化和反序列化一个类时是先调用writeObject和readObject,如果没有默认调用的是ObjectOutputStream的defaultWriteObject以及ObjectInputStream的defaultReadObject方法。
10,树中当两个节点的hash一样,会利用compareTo方法比较,如果还是相同,就使用identityHashCode进行比较。
JDK1.7和1.8的HashMap的不同点
(1)在位于同一个bucket时,JDK1.8是使用的尾插法,能够避免出现逆序且链表死循环的问题。JDK1.7是单链表进行的纵向延伸,采用头插法能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。
(2)扩容后数据存储位置的计算方式也不一样:
在JDK1.7的时候是直接用hash值和扩容后容量的二进制数进行&即hash值 & length-1 。
而在JDK1.8的时候是 原索引位置+扩容的大小值=新的位置。这种方式只需要判断Hash值的新增参与运算的位是0还是1就可迅速计算出了扩容后的索引位置。
(3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(N)变成O(logN),提高了查找效率)。
hashMap的长度为什么一定是2的次幂?
-
会使得在数组中位置即索引更加均匀
我们看到,上面的&运算,高位是不会对结果产生影响的(hash函数采用各种位运算可能也是为了使得低位更加散列)。我们只关注低位bit,如果低位全部为1,那么对于h低位部分来说,任何一位的变化都会对结果产生影响,也就是说,要得到index=21这个存储位置,h的低位只有这一种组合。这也是数组长度设计为必须为2的次幂的原因。
如果不是2的次幂,也就是低位不是全为1此时,要使得index=21,h的低位部分不再具有唯一性了,哈希冲突的几率会变的更大,同时,index对应的这个bit位无论如何不会等于1了,而对应的那些数组位置也就被白白浪费了。 -
扩容时便于快速计算新的存储位置(java8中采用的索引的新计算方式)
只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap。
HashMap和HashTable的区别
- HashMap可以存储null值(value)和null键(key),而Hashtable不行。
- HashMap是非线程安全的,而Hashtable是线程安全的,这也意味着性能也会差点。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
HashMap为什么是线程不安全的?
HashMap 在并发时可能出现的问题主要是两方面:
- put的时候导致的多线程数据不一致
比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
- resize而引起死循环
这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。