HashMap的存储结构
JDK 1.8之前
在JDK 1.7,HashMap采用的是数组+链表(头插法) 的存储结构。HashMap通过key的hashCode经过hash方法处理过后得到hash值,然后通过 (n - 1) & hash 判断当前元素存放的位置(n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存在的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。JDK 1.8之前的hash方法如下:
1 | static int hash(int h) { |
存储结构如下:
所谓 “拉链法” 就是将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
JDK 1.8
JDK 1.8之前的HashMapHashMap采用的是数组+链表(头插法)的存储结构,在链表很长时查询效率明显降低o(n),所以JDK 1.8做了很多优化采用数组+链表(尾插法)+红黑树的存储结构。红黑树的查询时间复杂度是o(log n)。当链表长度大于阈值(默认8)时,如果当前数组的长度小于阈值(默认64),那么会先进行数组扩容,否则会把链表转化为红黑树,以提高查询效率。当红黑树上的元素个数减少到阈值(默认6)时,会再转为链表。JDK 1.8的hash方法如下:
1 | static final int hash(Object key) { |
相对于JDK 1.7的hash方法的性能会稍微差一点点,因为扰动了4次。
存储结构如下:
常用变量
在 HashMap源码中,比较重要的常用变量,主要有以下这些。还有两个内部类来表示普通链表的节点和红黑树节点。
1 | //默认的初始化容量为16,必须是2的n次幂 |
小结
-
HashMap是无序且线程不安全的
-
HashMap可以存null键和值,key重复会被覆盖,value可以重复
-
HashMap每次是成倍扩容,且长度都是2的幂次方
这是为了尽量减少hash冲突考虑的。如果自己传的长度到HashMap构造方法,会返回大于等于参数的最小2次幂。1
HashMap<String,String> map = new HashMap<>(13);//容量为16
-
JDK 1.7的HashMap在多线程环境下可能会出现链表死循环
原因是采用头插法,多线程推荐使用ConcurrentHashMap(JDK 1.7使用Segment,JDK 1.8使用synchronized和CAS,synchronized只锁定当前链表或红黑树的首节点,效率提升n倍)