Java容器系列--HashMap源码阅读

HashMap

版本:1.8

随便new一个HashMap然后进入其中.

文中不复制过多的源码,可以比较着看.

1.结构分析

1.1 变量:

所有的变量都在这里了

/**
 * defaultInitialCapacity
 * 默认初始容量(16),
 * 用来新建map容器时作为参考.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
 * maximumCapacity
 * 最大容量(1<<30)
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * defaultLoadFactory
 * 默认加载因子(0.75)
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * treeifyThreshold
 * 树起点.(8)
 * 由链表转化为红黑树存储的阈值.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * unireeifyThreshold
 * 不是树的起点.(6)
 * 转化为链表存除的阈值.
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * minTreeifyCapacity
 * 最小树容量(64)
 * 当桶中的结构转为红黑树的最小table大小.
 * 当table<64 && hash冲突太多时,改用扩容操作,而不是转化为红黑树存除.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 下面四个变量都用的transient修饰,表示的是不需要进行序列化.
 * 都是临时存在的.
 */
//保存Node节点的数组,在首次使用时初始化,并根据需求调整大小.
//我们文中提到的桶指的就是该数组.
//这个桶表示的是链表||红黑树起始节点的数组,比如三个<K,V>,可能只有两个在table中.其中一个因为冲突,在以table位起点的链表中.
transient Node<K,V>[] table;

//存放元素的集合
transient Set<Map.Entry<K,V>> entrySet;

//记录hashMap的大小
transient int size;

//mod的计数器,用来修改map结构.
transient int modCount;

//临界值 当实际大小(容量*加载因子)超过临界值时,进行扩容
int threshold;

//加载因子.在扩容的时候需要,计算threshold=(CAPACITY*loadFactory)
final float loadFactor;

1.2 内部类

可以看出来大概分了三类:

  • 迭代器的一类:iteratorSpliterator各4个.
  • 赋值的一类:key,value,entry有3个.
  • 内部结构的一类:NodeTreeNode分别表示链表结构和红黑树结构.

迭代器和赋值先忽略,我们重点分析一下结构.

1.2.1关于Node<K,V>

自己查看源码可发现.实现了Map.Entry<K,V>接口.是一个单项链表(只有一个next).

1.2.2 关于TreeNode<K,V>.

继承了LinkedHashMap.Entry<K,V>类.实现方法有很多.通过其中的变量可以看出来是一个红黑树map.

然后我们通过从构造,到增删改查,再到调整结构来分析一下各个参数的作用.

1.3 工具方法

HashMap中有一些基本的方法.

  • hash(Object key):我们重新得到了一个Object的哈希值:通过高16位和低16位异或运算得出的哈希值.
  • 求位置运算:我们在源码中常见的一个运算是((n-1)&hash).这个运算的目的是为了通过对桶的大小取模,找出该元素所对应的哈希桶的位置.然后再进一步的寻找.

2. 流程分析.

2.1 构造方法

HashMap中重载了4个构造方法.可以看出来,都是设置loadFactorythreshold属性,这个属性就是我们上面提到的加载因子初始容量大小.

然后转化为合法的大小.比如容量只能是2的整次幂.通过一些运算将容量合法.

PS:这个获取合法初始容量大小的方法tableSizeFor也小优化了一下.通过1,2,4,8,16的五次位运算.就可以求出最小的值.确实厉害.(有兴趣的自行了解一下,并不难)

2.2 查找.

查找就是普通的查找方法,没什么难度的.

可以看出来,当我们调用get(Object key)方法后,

  1. 首先通过hash(Object key)获得了这个key的哈希值,
  2. 然后通过哈希值和这个key调用了真正的查找函数getNode(int hash,Object key).
  3. 这个查找函数经过查找后返回一个<K,V>对象.然后我们再返回value.

进入getNode()函数

  1. tab是用来确定长度,first确定位置,e是目标键值对.
  2. 首先找到键值对所在桶的位置.((n-1)&hash)是对hash取模,因为位运算要快于模运算.
  3. 判断桶中第一个元素是否是我们要找的.如果是的话就返回first.
  4. 桶中不止一个节点的话,通过instanceof关键字判断一下是否是红黑树,如果是的话,通过getTreeNode来找,否则直接遍历这个链表然后查找.

TreeNode中的getTreeNode.

  1. 如果当前节点是根节点,就直接在当前所在的红黑树中调用find,否则找到根节点调用find.
  2. 在红黑树中也是,左右子节点伸下去.,找到目标节点就返回TreeNode对象.

总之,查找就是根据hash和key来判断

2.3 添加

首先找到put(K key,V value)函数.看到了它调用了putVal函数,并且传入了hash值.

putVal中分析:

  1. 先定义了变量,tab表示桶,p表示要添加的元素.n表示的数组长度.
  2. 在第一个if中我们知道,先判断了桶中元素是否为空,如果为空的话需要进行扩容操作resize(),然后重新赋值tabn.
  3. 通过求位置运算(上文中介绍了)可以得到该元素在桶中的位置,如果没有发生hash碰撞,就直接将这个点存进去,否则进行else步骤.
  4. else中:hash值相同,但是key不同的时候,表示的是该元素在链表||红黑树中.
    1. 我们定义了两个变量:e表示的是要修改的目标节点,k表示的是key.
    2. 首先进行了一个判断起使位置是否是目标节点.如果是目标节点直接得到e.
    3. 然后判断是红黑树节点还是链表.红黑树调用putTreeVal方法.
    4. 如果是链表遍历链表,
      1. 如果不存在key,就在末尾添加一个.然后判断一下是否需要树化treeifBin().
      2. 如果遍历中存在key,直接获取e.
    5. 最后如果e不为空,表示的就是原HashMap中存在一个相同key的节点e,我们将其修改一下即可.
  5. 所有操作都做完后,判断一下是否需要扩容(size>threshold).

可以看出来,putVal()函数主要干了4件事:

  1. table合理.
  2. 向对应的位置放元素,如果是链表则放入链表,如果是红黑树则放入红黑树.
  3. 链表的如果需要转化为红黑树的转化为红黑树.
  4. 再进行扩容.

流程用一个网图来说明:侵删https://upload-images.jianshu.io/upload_images/5982616-c1b9d345bc371797?imageMogr2/auto-orient/strip|imageView2/2/format/webp
看一下扩容函数resize().

  1. 定义了几个变量.oldTab表示旧的table表(当第一次添加元素的时候位null).oldCap表示旧的容量,oldThr表示旧的扩容阈值,然后定义新的容量newCap和新的阈值newThr.
  2. 第一个判断:合法的扩容处理.
    1. 如果oldCap>0,则将新的容量和阈值都扩大为原来的2倍.
      1. 如果已经扩到了MAXINUM_CAPACITY,则扩容阈值设置为Integer.MAX_VALUE.不进行扩容.直接返回oldTab.
      2. 否则看一下是否到了最大容量(newCap=oldCap<<1)且设置新的阈值newThr=oldThr<<1.
      3. 如果都不是则不设置新的阈值.
    2. 如果oldThr>0,表示我们在构造函数中设置了初始的大小.直接更新newCap=oldThr即可.
    3. 否则表示第一此初始化容器,使用默认参数构造newCapnewThr.
  3. 第二个判断:如果新的扩容阈值为空,表示当前table为空,但是有阈值.
    1. 计算新阈值ft=newCap*loadFactory.
    2. 如果新的容量小于2^30&&ft<2^30.就将ft赋值给newThr.否则为Integer.MAX_Value.
  4. 我们获得了所有的更新后的容量和阈值之后.开始进行扩容.
  5. 申请一个新的容量的newTab,将其赋值给全局的table.
  6. 如果老数组不为空我们则需要进行赋值操作.
    1. 遍历每个老数组中每个位置的链表或者红黑树,重新计算节点位置,然后插入新数组.
    2. 对于每一个oldTab中存在元素的位置.我们先释放原数组中的元素.
    3. 如果此时,该节点是一个独立的(e.next==null),则直接计算新的桶中的下标(e.hash&*(newCap-1)).插入其中.
    4. 如果该节点为红黑树节点,则需要根据split()函数进一步确定该节点在新数组中的位置.
    5. 否则就是拷贝链表.
      1. 定义了四个节点,分别是低位头尾和高位头尾.
        为什么会有低位和高位之分?
        我们对原数组进行扩容,我们的容量增大一倍,那么我们原来冲突的hash值现在可能不冲突了.分成了高位和低位两种情况.
      2. 这里的e.hash&oldCap可以判断出该节点应存放的位置.等于0表示在低位,大于0表示放在高位.然后重新放入链表
      3. 将所有的元素都放完后,分别判断低位和高位的Tail.并且将其Head放入newTab中.
  7. 最后返回newTab.

可以看出来,扩容函数resize()主要干了两件事:

  1. 获得一个新的合理的容量newCap和阈值newThr.
  2. 申请完新的table后进行值的拷贝.注意了高位和低位的分开.

2.4 修改

HashMap中没有update等操作.我们可以通过put()来修改.

2.5 删除

查看remove(Object key)函数.调用removeNode()函数,返回删除节点的value.

注意这里有两个remove().分别是remove(Object key)remove(Object key,Object value).观察发现其区别就在于调用removeNode()函数中参数matchValuemovable.

matchValuetrue表示只有当Value和传入的value相同时才删除节点.

movabletrue表示删除

查看removeNode():

  1. 前半部分和我们的查找类似.显示找到目标节点node.
  2. 接下来判断是否是我们要删除的节点.查看matchValue参数比较.
  3. 如果是红黑树节点,调用removeTreeNode()函数.
  4. 然后将这个节点删除,向后推一位.
  5. 更新modeCountsize.

参考资料

HashMap源码分析:https://www.jianshu.com/p/2e2a18d02218

容器类框架分析-HashMap源码分析:https://www.jianshu.com/p/4f7add8ed8f5

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章