以下HashMap源码的解析都是基于java8来讲解的。
HashMap的结构是数组加链表的形式(jdk7中也是),在java8中引入了红黑树,由于红黑树的时间复杂度是O(log n),引入红黑树是为了解决在哈希冲突很严重的时候导致链表太长,从而引起的查找效率太低的问题。
常量
※ 为什么链表长度大于8才进行树化
源码中有下面一段解释
大体意思就是:由于TreeNode所占空间是普通Node的两倍,所以只有在bin包含足够多的node的时候才会转化为Tree(有TREEIFY_THRESHOLD决定)。当bin变的很小的时候,又会转为链表。当hashCode分布良好的时候,几乎用不到tree,hash冲突很小。但是在随机hashCode时,离散性差,会导致hash冲突严重,所以导致链表很长,这时候就要转化为红黑树来提高查询效率。按上面java官方给出的概率,链表长度达到8的概率是0.00000006,是很低很低的概率了,所以java也是通过大概率统计得出大于8的时候才转化为红黑树。
内部类 Node
从java8开始,HashMap的节点改为了使用Node(java7使用Entry),其实都差不多,内部结构都类似。
hash hash值key key值value value值next 下一个节点方法
put方法
put方法调用了下面的putVal方法,这里和java7版本的不同有两处:
当链表长度大于8的时候,会转化为红黑树;而且如果Node是TreeNode类型,则按照红黑树的方式进行putput采用的是尾插法,这样在扩容的时候避免了多线程情况下出现死循环(java7采用头插法,会存在扩容时出现死循环)
get方法
get方法调用了getNode方法
HashMap扩容
HashMap的扩容存在的问题也是面试中经常问到的,首先来看下扩容的源码
在这段代码中,java8进行了优化的,一部分是元素位置不变的,一部分是元素位置+old capacity。它是这样实现的:
e.hash & oldCap
这段代码不是为了判断元素所在的位置,而是判断hash值在old capacity的那一位是不是0,举个例子:hashCode是20,old capacity是16,new capacity是32,计算元素位置的方式:(n - 1) & hash
扩容前后对比发现,差距就在old capacity二进制1的位置那,所以e.hash & oldCap可以判断出那个位置是否为1。如果为1,那么元素的新位置就是old capacity+扩容前的元素位置 即为扩容后的位置;如果为0,则扩容后的位置与扩容前相同。java8之所以这样优化,首先是因为HashMap的capacity始终是2的幂次方,这样就保证了e.hash & oldCap的正确性;其次这样优化去掉了扩容时的重新hash运算,提高了效率。
扩容存在的问题
数据覆盖看resize源码可以发现,当两个线程同时走到#1的位置时,如果线程1和线程2的key的hash值相同,那线程1赋值后让出cpu,此时线程2获得时间片,同样也进行了赋值操作,那么线程1赋的值就被线程2给覆盖掉了。死循环在java8中已经不存在死循环了。但是在java7中是存在的。有与java7是头插法,所以在resize的时候,链表的顺序会反转,此时两个线程同时进行resize,就有可能形成链式的圆圈,造成死循环。HashMap线程不安全,那该用什么
HashMap的线程版本有HashTable和Collections.synchronizedMap(map),但是这两个都是直接加synchronized实现的,效率很低。而HashMap的线程安全版本常用的是ConcurrentHashMap,这个也是面试中经常问到的。明天会更新ConcurrentHashMap的相关面试点,欢迎大家继续关注。