4

map是个key-value的存储结构,常用的实现有两种,HashMap和TreeMap。

HashMap

存储

hashmap是通过hash方式实现的map,底层存储在一个数组中。
我们来按照正常的操作模拟下hashmap的实现过程

Map map = new HashMap();

hashmap在初始化时,默认会创建一个长度为16的数组。
image.png

map.put(1, "A");

当我们调用put方法时,hashmap会获取key的hash值,如图红框,他会用key的hash与长度做一个与操作,算出的值就是数据存在数组的下标。
image.png
image.png
&和求余的比性能更高,但是因为是和数组的长度计算,所以要求数组长度必须是2^n,当我们初始化指定的长度不是2^n,它会如下图做一个转换。这时考点,注意记牢哦
image.png

hash碰撞

这就有种情况,当我们两个key算出的下标是一个位置时怎么办?
就是常说的hash碰撞,在jdk8之前,hashmap采用了链表的方式解决碰撞问题,当array[index]有值时就会把新数据作为一个链表的节点挂在上面。
image.png
这种方式就会存在链表明显的问题,时间复杂度O(n),所以在jdk8之后就把链表改成了红黑树。具体实现可参考上图中的源码。但并不是hash一碰撞就是红黑树,而是在碰撞数小余8时,是链表,到8个时才会转化成红黑树(如下图),但如果map的数组小余64时,也是不会转化成树,而是进行一次扩容。
image.png
当我们调用map的put方法时,hashmap是用equals方法判断两个key是否相等,相等则覆盖,但因为需要用hash算下标,所以当我们想把对象做key时,既要重写equals方法,也要重写hashcode方法

扩容

既然hashmap底层使用数组存储,自然也要涉及到扩容的问题,我们看看hashmap时如何实现的。
hashmap计算新数组长度的方式就是用旧的长度向左移1位,其实就是新数组是旧数组的2倍。
image.png
什么时候扩容呢?
上面我们已经说了一种情况,就是碰撞数>=8,但数组小余64,这种时候会扩容。
第二种就是当存储的 数据个数> 数组长度*0.75(就是常说的加载因子),默认的加载因子是0.75,我们在初始化hashmap时也是可以指定的。
扩容的方式就比较简单了,就是把旧数组中的数据重新计算hash(rehash),然后放到新数组中,rehash是比较消耗性能的,所以我们要尽量减少rehash的出现。

ConcurrentHashMap

ConcurrentHashMap与HashMap的区别就是ConcurrentHashMap是线程安全的,具体的实现方式会在后面的文章中详细介绍。

LinkedHashMap

因为HashMap的key是无顺序的,当我们需要实现类似LRU算法功能时,就可以用到就可以用到LinkedHashMap为我们提供了一个有序的hashmap了,LinkedHashMap为我们提供了一个有序的hashmap,LinkedHashMap其实就是另外维护了一个链表,每次操作时,重新把节点放置到链表尾部,如图
image.png
这三个方法时HashMap专门给LinkedHashMap留的后门。

TreeMap

treemap是一个标准红黑树的实现,红黑树的原理太长,这里就先不讲了。相比hashmap,treemap是一个可以排序的map,但操作的时间复杂度O(logn),比hashmap性能稍差,但没有扩容问题,所以如果需要有序的map,选择treemap,否则请使用hashmap

ConcurrentSkipListMap

ConcurrentSkipListMap是通过跳表实现的map,原理后面的数据结构章节再讲。
ConcurrentSkipListMap是一个线程安全且有序map,但操作的时间复杂度O(logn),存储空间大约也需要2倍。
所以需要线程安全map,要求有序选择ConcurrentSkipListMap,否则选择ConcurrentHashMap


搬砖的张飞
17 声望4 粉丝

java开发十年