HashMap详解

8 篇文章 0 订阅

目录

1. HashMap基础

2. Hash表

3. HashMap put()方法

4. JDK1.7和JDK1.8的区别

5. 为什么jdk1.8将尾插法改为头插法?

6. 为什么jdk1.8将链表改为红黑树?

7. hashMap的扩容机制

8. HashMap是线程安全的吗?怎么解决?

9. HashTable

10. ConcurrentHashMap

11. ConcurrentHashMap和HashTable有什么区别?能否取代HashMap?

12. ConcurrentHashMap的分段锁的实现原理

13.  HashMap是有序还是无序的,有序的map有哪些

14 . LinkedHashMap

15.  TreeMap

16. HashSet



1. HashMap基础

HashMap 以 key-value 键值对的形式存储,主干是一个Entry数组(初始值是空数组{},其长度一定是2的次幂)。每个Entry包含一个 key-value 键值对。

HashMap 最大容量是2^30 (32位是2^31,其中有一为是符号位,所以是2^30)

一般如果new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。

HashMap 用 key 做哈希定位,将 key 和 value 一起存到对应的位置。

Map其实就是保存了两个对象之间的映射关系的一种集合。

 HashMap总体结构如下:

2. Hash表

哈希表(Hash table,也叫散列表),是根据 key 值而直接进行访问的数据结构。也就是说,它通过把 key 值映射到表中一个位置来访问记录,以加快查找的速度。若关键字为k,则其值存放在 f(k) 的存储位置上。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。

  • 求余法
    • 取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
  • 直接寻址法
    • 取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。若其中H(key)中已经有值了,就往下一个找,直到H(key)中没有值了,就放进去。
  • 数字分析法
    • 分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
  • 平方取中法
    • 当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。

哈希冲突(主要解决求余法导致的冲突) :

  • 拉链法(余数一样的放一起 连着放)
  • 开放地址法
    • :Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
      • di=1,2,3,…,m-1,称线性探测再散列;
      • di=1^2,-1^2,2^2,-2^2,⑶^2,…,±(k)^2,(k<=m/2)称二次探测再散列;
      • di=伪随机数序列,称伪随机探测再散列。

3. HashMap put()方法

  • 首先判断table数组是否为空,为空就进行初始化,初始容量是16。

  • 不为空的话,用数组长度 n-1与key的哈希值进行与操作,得到的结果作为下标索引,判断table数组此下标的值是否为空,为空就构造一个Node,将想要put的数据放在这个位置。

  • 不为空的话,说明发生了哈希冲突(两个 key 的哈希值一样),判断 key 是否一样,一样的话就将新值覆盖旧值。

  • 不一样的话,判断当前节点是否是树形节点,是的话就插入红黑树中。

  • 如果不是树形节点的话就插入链表中,然后判断链表长度是否大于8并且数组长度是否大于64,如果都满足的话,就将链表转化为红黑树。

  • 插入完成后,判断节点数是否大于阈值,如果大于的话就进行扩容,将数组长度扩大为原来的二倍。

4. JDK1.7和JDK1.8的区别

1.7:

  • 头插法
  • hash()-->4次扰动
  • 数组+链表
  • 先扩容再插入

1.8:

  • 尾插法
  • hash()-->1次扰动
  • 数组+链表/红黑树
    • 当前链表长度大于8 ,数组长度大于64时转化为红黑树,节点变成树节点,以提⾼搜索效率和插⼊效率到 O(logN)。

  • 先插入再扩容

5. 为什么jdk1.8将尾插法改为头插法?

JDK1.7 先扩容再插入时,采用头插法,头插法在单线程时不会出现问题,但是在多线程时会产生死循环的问题。 

具体的原理请看:JDK1.7的HashMap头插法循环问题

6. 为什么jdk1.8将链表改为红黑树?

        链表的时间复杂度是O(n),红黑树的时间复杂度O(logn),很显然,红黑树的复杂度是优于链表的,红黑树拥有更高的查找性能。

为什么不直接使用红黑树?

        因为树节点占用空间是普通节点的两倍,如果一开始使用红黑树的话,节点数量较少时,会使空间消耗较大,只有当节点数量足够多时,才会让红黑树消耗空间大这一劣势不太明显。所以当节点数量较少时使用链表,节点数量较多(链表长达大于8,数组长度大于64)时使用红黑树。

为什么是大于8才扩容?

        一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,是根据概率统计决定的,而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树,提高性能。

1.8链表红黑树节点转化机制:链表节点数大于8转化为红黑树,节点数小于6转化为链表。

红黑树(核心是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。)

  • 心是黑的-->根节点是黑的

  • 要么红要么黑-->节点只有这两种颜色

  • 两颗红心不能在一起-->从每个叶子到根的所有路径上不能有两个连续的红色节点

  • 所有路径经过的黑色节点数目一致(所有看得见的叶子节点下还有两个子节点为空,这才是真正的叶子结点)

  • O(logn)

  • 近似平衡二叉树,平衡二叉树的特点:

    • 所有节点最多拥有两个子节点,即度不大于2;

    • 任意一个节点的左右子树高度差不大于1

    • 左子树的键值小于根的键值,右子树的键值大于根的键值;

7. hashMap的扩容机制

当数组长度达到(最大程度 * 负载因子(0.75))的就会扩容数组。扩容大小为原数组的倍。

扩容引子为什么为0.75?

  • 负载因子过大,虽然空间利用率上去了,但是时间效率降低了(大量的hash冲突)。

  • 负载因子太小,虽然时间效率提升了,但是空间利用率降低了

  • 负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

hashmap扩容为什么是2倍?

        HashMap计算添加元素的位置时,使用的(hash运算)位运算,这是特别高效的运算;另外,HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!
位运算 :当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

8. HashMap是线程安全的吗?怎么解决?

不是,在多线程的情况下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题。

如何解决:

Java中有hashTable(哈希表),ConcurrentHashMap可以实现线程安全的Map。

HashTable(全段锁)是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大,ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。

9. HashTable

  • Hashtable(线程安全,对整个结构加锁,性能不好)

  • 同样为Key-Value数据结构,但不允许key,value的值为null

  • 默认数组长度为11

  • 实现原理:数组+链表

10. ConcurrentHashMap

        ConcurrentHashMap 是线程安全的,它采⽤分段锁技术,将整个Hash桶进⾏了分段segment,也就是将这个大的数组分成了几个小的⽚段 segment,⽽且每个小的⽚段 segment 上⾯都有锁存在,那么在插⼊元素的时候就需要先找到应该插入到哪⼀个⽚段 segment,然后再在这个片段上⾯进行插⼊,⽽且这⾥还需要获取 segment 锁,这样做明显减小了锁的粒度。 Segment 的个数一但初始化就不能改变。

JDK 7中, ConcurrentHashMap 采⽤了数组 + Segment + 分段锁的⽅式实现。

JDK 8中 ,ConcurrentHashMap 参考了 JDK 8 HashMap 的实现,采⽤了数组 + 链表 + 红⿊树的实现⽅式来设计,内部⼤量采⽤ CAS 操作。

        Java8 中的 ConcruuentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

11. ConcurrentHashMap和HashTable有什么区别?能否取代HashMap?

主要区别体现在实现线程安全的方式上。

HashTable 和 ConcurrentHashMap 相⽐,效率低。

Hashtable 之所以效率低主要是使⽤了 synchronized 关键字对 put 等操作进⾏加锁,⽽ synchronized 关键字加锁是对整张 Hash 表的,即每次锁住整张表让线程独占,致使效率低下。

ConcurrentHashMap JDK1.7时在对象中保存了⼀个 Segment 数组,即将整个 Hash 表划分为多个分段; ⽽每个Segment元素,即每个分段则类似于⼀个Hashtable;这样,在执⾏ put 操作时⾸先根据 hash 算法定位到元素属于哪个 Segment,然后对该 Segment 加锁即可,因此, ConcurrentHashMap 在多线程并发编程中可是实现多线程 put 操作。 到了JDK1.8,直接摒弃了Segment的概念,直接采用Node数组+链表+红黑树来实现,并发控制使用synchronized和CAS来操作。

        不能取代HashMap,Hashtable的任何操作都会把表锁住,是阻塞的,好处是总能够获取最实时的更新,ConcurrentHashMap为非阻塞的,在更新时会局部锁住某部分数据,但不会把整个表都锁住,同步读取操作是完全非阻塞的,在合理条件下效率非常高,坏处是在大量的读取操作时不能保证数据的实时更新。

12. ConcurrentHashMap的分段锁的实现原理

ConcurrentHashMap 采⽤了⾮常精妙的"分段锁"策略,ConcurrentHashMap 的主⼲是个 Segment 数组。

final Segment<K,V>[] segments;

        Segment 继承了 ReentrantLock,所以它就是⼀种可重⼊锁(ReentrantLock)。在 ConcurrentHashMap,⼀个 Segment 就是⼀个⼦哈希表,Segment ⾥维护了⼀个 HashEntry 数组,并发环境下,对于不同 Segment 的数据 进⾏操作是不⽤考虑锁竞争的。就按默认的 ConcurrentLevel 为 16 来讲,理论上就允许 16 个线程并发执⾏。 所以,对于同⼀个 Segment 的操作才需考虑线程同步,不同的 Segment 则⽆需考虑。Segment 类似于 HashMap,⼀个 Segment 维护着⼀个HashEntry 数组:

transient volatile HashEntry<K,V>[] table

        HashEntry 是⽬前我们提到的最⼩的逻辑处理单元了。⼀个 ConcurrentHashMap 维护⼀个 Segment 数组,⼀个 Segment 维护⼀个 HashEntry 数组。因此,ConcurrentHashMap 定位⼀个元素的过程需要进⾏两次 Hash 操 作。第⼀次 Hash 定位到 Segment,第⼆次 Hash 定位到元素所在的链表的头部。

13.  HashMap是有序还是无序的,有序的map有哪些

hashmap是无序的,是根据hash值随机插入的。

有序的map有LinkedHashMap 和 TreeMap。

14 . LinkedHashMap

        LinkedHashMap 也是基于 HashMap 实现的,  LinkedHashMap可以认为是HashMap+LinkedList,也就是说,它使用HashMap操作数据结构,也用LinkedList维护插入元素的先后顺序。

        不同的是它定义了⼀个 Entry header,这个 header 不是放在 Table ⾥,它是额外独⽴出来的。LinkedHashMap 通过继承 hashMap 中的 Entry,并添加两个属性 Entry before,after 和 header 结合起来组成⼀个双向链表,来实现按插⼊顺序或访问顺序排序。 LinkedHashMap 定义了排序模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;对于插⼊顺序,则为 false。⼀般情况下,不必指定排序模式,其迭代顺序即为默认为插⼊顺序。

LinkedHashMap的特点:

  • key和value都允许为空

  • key重复会覆盖,value可以重复

  • 有序的

  • LinkedHashMap是非线程安全的

15.  TreeMap

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

16. HashSet

        HashSet 的实现是依赖于 HashMap 的,HashSet 的值都是存储在 HashMap 中的。在 HashSet 的构造法中会初始化⼀个 HashMap 对象,HashSet 不允许值重复。因此,HashSet 的值是作为 HashMap 的 key 存储在 HashMap 中的,当存储的值已经存在时返回 false。

HashSet 怎么保证元素不重复的?

public boolean add(E e) {
     return map.put(e, PRESENT)==null;
 }

        元素值作为的是 map 的 key,map 的 value 则是 PRESENT 变量,这个变量只作为放⼊ map 时的⼀个占位符⽽存 在,所以没什么实际⽤处。其实,这时候答案已经出来了:HashMap 的 key 是不能重复的,⽽这⾥HashSet 的元素⼜是作为了 map 的 key,当然也不能重复了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值