通过源码分析各种Map(含LinkedHashMap、IdentityHashMap、ConcurrentHashMap)

本文深入解析Java HashMap的内部实现,涵盖源码分析、冲突解决策略、树化过程以及并发问题。首先介绍了HashMap的基本结构,包括树存储和链表到红黑树的转换。接着详细探讨了HashMap的构造函数、put()、get()方法的源码,解释了扩容机制和线程不安全问题。同时对比了HashMap与Hashtable、IdentityHashMap以及ConcurrentHashMap的差异。最后,讨论了WeakHashMap的特点和应用场景。文章强调了HashMap在多线程环境下的问题,并推荐了ConcurrentHashMap作为线程安全的选择。
摘要由CSDN通过智能技术生成


【本篇是集合中的Map篇,以下涉及源码基于JDK1.8】

(为何上一篇介绍了Collection下的list,不接着介绍Collection下的set,因为Set集合实际上就是HashMap来构建的!没看过List篇的移步这里了)

在这里插入图片描述

     本篇是Java基础中最重要的知识点集合Map篇。Java集合是java提供的工具包,包含了常用的数据结构:集合、链表、队列、栈、数组、映射等。Java集合工具包位置是java.util.*,Java集合主要可以划分为4个部分:List列表、Set集合、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)。接下来我们一起学习吧,博主水平有限,哪里有不对的地方欢迎大佬指出斧正。

     Colletcion和Map结构如下图所示:
在这里插入图片描述
     大致介绍一下:Collection和Map是两个高度抽象的接口

  • Collection抽象的是集合,包含了集合的基本操作和属性,Collection主要包含List和Set两大分支。
  • List是有序的链表,允许存储重复的元素,List的主要实现类有LinkedList, ArrayList, Vector, Stack。
  • Set是不允许存在重复元素的集合,Set的主要实现类有HastSet和TreeSet(依赖哈希实现,后面介绍)。
  • Map是一个映射接口,即存储Key-Value键值对的集合(和redis存储类似),AbstractMap是个抽象类,它实现了Map接口中的大部分API,而常见的HashMap,TreeMap都是继承于AbstractMap。HashTable继承于Dictionary,但也实现了Map接口。

     集合是Java中用来存储多个对象的一个容器,我们知道容器数组,数组长度不可变,且只能存储同样类型的元素,数组可以存储基本类型或者引用类型;而集合长度可变,可以存储不同类型元素(但是我们一般不这么干),集合只能存储引用类型(存储的基本类型会变成包装类);

集合的Fail-Fast机制?

     fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。

例如:当某一个线程A通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException 异常,产生 fail-fast 事件;当然,不仅是多个线程,单个线程也会出现 fail-fast 机制,比如单线程下的iterator迭代器遍历时调用集合的增删改等操作会抛出java.util.ConcurrentModificationException,从而产生fail-fast机制。

HashMap

     HashMap定义:

	public class HashMap<K,V> extends AbstractMap<K,V>
	    implements Map<K,V>, Cloneable, Serializable { }

     HashMap简介:

  • HashMap是基于Map接口的Key-Value的集合,允许使用null值和null键,但不保证映射的顺序,特别是它不保证该顺序恒久不变。HashMap在底层将Key-Value当成一个整体Entry来处理。

  • 底层使用数组实现,数组中每一项是单向链表,即数组和链表的结合体;链表长度大于一定阈值时,链表转换为红黑树。后面详细分析源码介绍

  • 在链表和数组中可以说是有序的(存储的顺序和取出的顺序是一致的),但同时也带来缺点:想要获取某个元素便要访问所有的元素直到找到为止(List中知道具体位置可以直接访问)。HashMap可以不在意元素的顺序,能够快速的查找定位到元素。

  • 散列表HashMap会为每一个Key计算出哈希值,即散列码,根据这些散列码保存在对应的位置上。如下图所示的数组+链表实现,一个hash值会遇到被占用的情况(hashCode散列码相同,就存储在同一个位置上),这种情况是无法避免的,这种现象称之为:散列冲突。后面详细介绍冲突以及解决方法等
    在这里插入图片描述

     HashMap源码及重点关注问题:

  1. 属性和默认值:
    在这里插入图片描述
    在这里插入图片描述
    注意:初始容量太高和负载因子太低的话,遍历效率不太好。
  2. 构造函数:
    在这里插入图片描述
    这里我们需要注意第一个构造函数,传入初始容量和负载因子,如果初始容量小于0抛出异常,大于最大容量MAXIMUM_CAPACITY则置为MAXIMUM_CAPACITY;如果不是这俩个会进入tableSizeFor()方法,一起来看看:
    在这里插入图片描述
     int n = cap - 1;

为什么要有第一步的减一操作?

目的是为了防止传入的cap已经是2的幂,如果没有执行这个减一操作,返回的将是这个cap(传入是2的幂)的2倍,不太懂?请继续读下去。

     n |= n >>> 1;
     n |= n >>> 2;
     n |= n >>> 4;
     n |= n >>> 8;
     n |= n >>> 16;

     重点:如果传入的cap为0,经过减一和后面几次的无符号移动之后会一直是0,最后返回capacity是1;主要考虑cap不为0的情况,由于cap不为0,则n的二进制中总会有一个bit位为1,我们只考虑最高bit位为1。

     第一步n >>> 1是无符号右移1位,再做或运算,使得二进制中的最高位和紧邻的右边一位也是1(0000 11## #### ####这种格式)。

     第二步n >>> 2是无符号右移两位再做或操作,第一步已经将最高位和其临位变为1,这一步则会将从高位开始的4位都变成1(0000 1111 #### ####这种格式)。

     第三步n >>> 4是无符号右移4位再做或操作,同样的道理,经过这一步之后从最高位开始后面的8位都变为1(0000 1111 1111 ####这种格式)。

     以此类推,容量最大是32位正数,经过n |= n >>> 16之后最多也就32个1(1+2+4+8+16=31,但是别忘了开始的那一位),此时已经是负数了。

在执行tableSizeFor之前,对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(2 ^ 30),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作。所以这里面的移位操作之后,最大30个1,不会大于等于MAXIMUM_CAPACITY。30个1,加1之后得2 ^ 30) 。

     举个例子:

     现传入一个数cap为11,n=cap-1为10,则n二进制为0000 1010。

n |= n >>> 1:n >>> 1变为0000 0101,n或n>>> 1变为0000 1101,n即此值;
n |= n >>> 2:n >>> 2变为0000 0011,n或n>>> 2变为0000 1111,n即此值;
n |= n >>> 4:n >>> 4变为0000 0000,n或n>>> 4变为0000 1111,n不变;
n |= n >>> 8:n >>> 8变为0000 0000,n或n>>> 8变为0000 1111,n不变;
n |= n >>> 16:n >>> 16变为0000 0000,n或n>>> 16变为0000 1111,n不变;

看完上面可能会感到奇怪的是:为啥是将2的整数幂的数赋给threshold?

     threshold这个成员变量是阈值,决定了是否要将散列表再散列。它的值应该是:capacity * load factor才对的。其实这里仅仅是一个初始化,当创建哈希表的时候,它会重新赋值的。

  1. 内部类Node:
    在这里插入图片描述
  2. put()方法–放入(Key,Value)键值对:
    在这里插入图片描述
    内部包含hash()函数和putVal()函数,hash函数用来求哈希值,putVal()函数用来将key-value键值对放入集合map,我们先看hash()函数:
    在这里插入图片描述
    这里直接将key的哈希值返回不就好了, 为什么还要做异或运算?

我们进入putVal()函数看:
在这里插入图片描述
这里是根据key的哈希值来保存在散列表中的,刚刚上面的key原哈希值和高16位异或运算得到的新哈希值便是用来计算保存在散列表中位置的。

我们表的默认初始容量是16,要放到散列表中即为0-15的位置,即tab[i = (n - 1) & hash]这里。我们可以发现在做&运算的时候,仅仅是后4位有效(容量32则是后5位有效,依次类推),那如果我们key的哈希值高位变化很大,低位变化很小,直接拿过去做&运算,这就会导致计算出来的Hash值相同的很多。

而设计者将key的哈希值的高位也做了运算(与高16位做异或运算,使得在做&运算时,此时的低位实际上是高位与低位的结合),这就增加了随机性,减少了碰撞冲突的可能性!

详细理解下HashMap的put()过程吧:
在这里插入图片描述
put源码中还可以挖掘更多细节,比如putTreeVal()在树中插入节点、treeifyBin()转变树结构等,感兴趣的可以多多研究,关于红黑树结构及原理,文章下面会介绍。

  1. resize()方法–扩容机制:
    在这里插入图片描述
         当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

          那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说&

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值