说一说Map

236 阅读6分钟
  • HashMap:initialCapacity默认为16,loadFactory默认为0.75

HashMap由数组+链表组成的,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry,这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap数组每一个元素的初始值都是Null。



对于HashMap,我们最常使用的是两个方法:Get Put

1.Put方法的原理

调用Put方法的时候发生了什么呢?

比如调用 hashMap.put("apple", 0) ,插入一个Key为“apple"的元素。这时候我们需要利用一个哈希函数来确定Entry的插入位置(index):

index = Hash(“apple”)

假定最后计算出的index是2,那么结果如下:


但是,因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。比如下面这样:


这时候该怎么办呢?我们可以利用链表来解决。

HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可:


需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”。至于为什么不插入链表尾部,后面会有解释。

2.Get方法的原理

使用Get方法根据Key来查找Value的时候,发生了什么呢?

首先会把输入的Key做一次Hash映射,得到对应的index:

index = Hash(“apple”)

由于刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”:


第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。

第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。

之所以把Entry6放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大

  • 1.8使用红黑树:防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);
  • 1.8使用尾插法:因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:

在这里插入图片描述





之前说过,从Key映射到HashMap数组的对应位置,会用到一个Hash函数:

index = Hash(“apple”)

为了符合Hash算法均匀分布的原则

那负载因子是个什么东西?负载因子其实就是规定什么时候扩容。上面我们说默认hashmap数组大小为16,存的键值对数量超过16则进行扩容,好像没什么毛病。然而HashMap中并不是等数组满了(达到16)才扩容,它会存在一个阀值(threshold),只要hashmap里的键值对大于等于这个阀值,那么就要进行扩容。阀值的计算公式:

阀值 = 当前数组长度✖负载因子

hashmap中默认负载因子为0.75,默认情况下第一次扩容判断阀值是16 ✖ 0.75 = 12;所以第一次存键值对的时候,在存到第13个键值对时就需要扩容了;或者另外一种理解思路:假设当前存到第12个键值对:12 / 16 = 0.75,13 / 16 = 0.8125(大于0.75需要扩容) 。

不知道java 1.7 的HashMap实现大家看出需要优化的点没有?

其实一个很明显的地方就是:

当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)

在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。

在这里插入图片描述

java1.8 HashMap数据查找实现思路:

1.判断表或key是否是null,如果是直接返回null

2.判断索引处第一个key与传入key是否相等,如果相等直接返回

3.如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值

4.如果不是树,就遍历链表查找

HashMap中红黑树的查找函数find()实现  :

blog.csdn.net/ymrfzr/arti…


ConcurrentHashMap

一般在多线程的场景,我都会使用好几种不同的方式去代替:使用Collections.synchronizedMap(Map)创建线程安全的map集合(通过加锁实现线程安全的);Hashtable(线程安全,效率低,Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。)ConcurrentHashMap  不过出于线程并发度的原因,我都会舍弃前两者使用最后的ConcurrentHashMap,他的性能和效率明显高于前两者。ConcurrentHashMap 采用了分段锁技术



LinkedHashMap

是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。



TreeMap 

TreeMap是按照Key的自然顺序或者实现的Comprator接口的比较函数的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较。