阅读 1192

【开源解码】之HashMap

HashMap源码剖析

JDK7

在jdk7中,HashMap由数组+链表实现

  • 数组初始大小16 阈值12(0.75*16)
  • 达到阈值之后,两倍扩容

put方法

第一次put会先创建数组 如果key是null,调用putForNullKey

  • 先取出数组第一个元素Entry
  • 遍历Entry,如果在链表上存在key为null的节点,替换value并返回旧的value
  • 否则将该元素添加到数组的第一个位置上

如果key不为null

  • 先对key值进行二次hash运算,得到hash
  • 然后根据通过位运算hash值找到数组下标i
  • 找到下标之后,对数组table[i]这个链表进行遍历
    • 如果链表中存在与当前插入元素相同的key值,新值覆盖旧值,并返回旧的值
    • 否则添加,调用addEntry方法,之后返回null

<img src="" alt="2" style="width:75%"/>

addEntry

先判断是否进行扩容

  • 元素达到阈值并且数组(也称为桶)的第一个元素不为null时才扩容
  • 极端情况下如果数组大小等于阈值并且第一个元素是null就不会扩容

然后调用createEntry方法插入元素,把新元素作为链表的头,并赋值到数组上

  • 值得注意的是,这是一个头插法,设计者认为新插入的元素更容易被访问到
  • 头插法指的是,先找到链表,取出链表EntryOld,然后创建一个新的链表EntryNew,然后把EntryNew的next域指向EntryOld

<img src="" alt="1" style="width:75%"/>

扩容的实现

  • 先resize()传入两倍数组的长度,
    • 创建两倍大小的新数组
    • 通过transfer遍历原来的数组,为每个链表元素找到新数组中的索引,调用头插法把所有的元素的引用存到新的数组
    • 然后把新的数组赋给原来的数组table
    • 然后重新设置阈值threshold
  • 然后通过indexfor找到插入元素在扩容新数组的索引
  • 之后调用createEntry方法插入元素

<img src="" alt="4" style="width:80%"/>

jdk7 put小结
3

get方法

如果key为null,调用getForNullKey并返回

  • 如果数组长度为0,返回null
  • 遍历数组中的第一个元素,对这个元素(链表)遍历,如果找到了key==null的就返回该元素的value
  • 如果没找到,返回null

如果key不为null,则调用getEntry方法找到元素

  • 先算出key的hash
  • 然后根据hash值找到数组索引index
  • 然后遍历数组对应索引下的链表table[index]

面试常问

  1. 数据结构? -> 数组+链表
  2. 如何添加数据的? -> 头插法
  3. 怎样预防和解决哈希冲突的?
    • 预防:二次哈希 or 扰动函数
    • 解决:通过其数据结构来解决->拉链法
  4. 默认容量? -> 16
  5. 内部数组什么时候创建的? -> 第一次put的时候创建的

JDK8

<img src="" alt="7" style="width:75%"/>

构造函数

  • 数组初始大小:16
  • 加载因子:0.75

put

内部调用了putVal方法

首次put
  • 1.首次put,先调用resize()方法,给数组初始化,初始化阈值
  • 2.然后判断元素放置在数组中位置i的tab[i]是否为null,如果null调用newNode创建一个新的node
    • 位置i是通过与运算计算出来的,与jdk7一样
  • 3.然后判断是否需要扩容,这与jdk7不同
    • jdk7是先赋值后创建
    • jdk8是先创建后赋值

整体流程如下
6

之后的put

不需要初始化数组等操作

  • 4.如果元素放置在数组中位置i的tab[i](也就是当前链表的头节点)是为null,则调用newNode创建一个新的node,与第一次类似
  • 5.如果当前元素放置在的位置tab[i]不为null,进入else代码块,else代码块会先创建局部变量e和k,e表示
    • 5.1 如果链表元素p(也就是tab[i],也是当前链表的头节点)与新插入的元素key相同,会直接替换掉原来的元素
    • 5.2 判断p是不是树节点,如果是树节点,调用树节点的putTreeVal方法
    • 5.3 否则就不是树节点,说明仍然是一个链表节点,
      • a.由于头节点在5.1步骤中已经处理过,所以直接遍历这个链表的后续节点
      • b.每一次遍历完成之后,都对e(局部变量)重新赋值
      • c.当遍历完成也就是p.next为null时,创建一个新的节点,插入到链表尾部
      • d.如果当前链表长度大于8,调用treeifyBin进行树化操作,break跳出
      • e.如果p.next不为空,进入下一个if,如果后续节点中找到了相同的key,直接覆盖
    • 5.4 如果e不为null,即替换了旧的元素,由于e的value还是旧的值,就将新的value赋给e

put小结 
<img src="" alt="9" style="width:80%"/>

treeifyBin树化操作
  • 先判断数组长度是否小于64,小于64扩容,大于等于64才树化
  • 先找到当前链表,然后遍历,dowhile循环将单链表转换成双向链表
  • 如果头节点不为null,调用treeify进行树化操作
    • treeify会遍历双向链表
    • 首先将双向链表的头节点作为红黑树的根节点,当然,后续根节点可能会发生改变
    • 然后通过不断遍历,生成红黑树(二叉平衡树的一种)
扩容机制
  • 1.先判断老的数组是否超过阈值,如果超过了直接返回老的数组
  • 2.如果没有达到阈值并且大于初始值16,数组长度变为原来的两倍,阈值赋值为原来的两倍
  • 3.然后创建新的数组
  • 4.然后对老的数组进行遍历,数组的每一个元素为头结点e
    • 4.1 如果e是一个单节点(没有后继节点),就将老数组中的元素赋值到新数组对应的位置上
    • 4.2 如果e是树节点,使用树节点的方法进行拆分赋值
    • 4.3 如果e是链表,
      • 首先通过dowhile循环内的与运算创建一个高位列表一个低位列表
      • 然后将低位列表赋值给新数组的低位,高位列表赋值给新数组的高位

面试常问

  1. 1.7和1.8数据结构有什么不同?
    • 1.8 增加了转换为红黑树
  2. 插入数据的方式?
    • 1.7 的链表从前面插入
    • 1.8 的链表从后面插入
  3. 扩容后存储位置的计算方式?
    • 1.7 通过再次 indexFor() 找到数组位置
    • 1.8 通过高低位的桶直接在链表尾部添加
  4. HashMap什么时候会把链表转化为红黑树?
    • 链表⻓度超过 8 ,并且数组⻓度不小于 64

其他常用数据结构

  • ArrayList:内部是数组结构
  • LinkedList:列表结构
  • TreeMap:二叉树
  • HashMap:
    • 数组+链表
    • jdk8以后:数组+链表,达到一定程度以后会转化成为红黑树
文章分类
后端
文章标签