HashMap实现原理

HashMap的原理及实现

  • 我的对HashMap的总结,有错误请留言.
  • 本文是纯文字介绍的,若是有朋友喜欢结合代码的话也能够直接点击文末连接。
  • 感谢阅读.

概述

  • HashMap是在JDK1.2中引入的一种K/V对形式的集合类.
  • 在底层,HashMap经过数组和单链表组合的结构形式来存储数据,数组在这做为一个外部结构,数组中的每一个节点被称作Bucket(桶),而桶是由在单链表构成,JDK1.8以后为了解决长链表下,查询和插入效率低下的状况,又引入了红黑树的做为桶的实现方式,
  • 桶中的各节点是由HashMap定义的Node内部类生成的,是普通的链表节点类.

HashMap的实现方式

  • 注意:HashMap是线程不安全的,在JDK1.8以前多线程状况下甚至可能会出现环路(后面会讲),因此多线程状态下仍是要使用ConcurrentHashMap的.

重点参数

  • HashMap的参数很少,除去当作默认属性的静态常量和底层数组对象,就只有如下五个
transient Node<K,V>[] table;
transient int size
transient int modCount; 
int threshold;
final float loadFactor;
复制代码
  • table就是整个HashMap的底层数组,table的初始化并不在构造函数中完成,而是在resize()方法中完成.html

    • table的初始化可能有点绕,构造函数中最多指定了阈值threshold和负载因子loadFactor并无容量相关,可是在resize()方法中会根据旧容量和旧阈值判断新容量是等于默认容量,旧阈值或者两倍旧容量,最后根据新容量建立新数组
  • loadFactor就是所谓的负载因子,默认为0.75,是控制扩容时机的关键属性,由于扩容发生在当前元素个数超过阈值时,而阈值等于当前容量乘以负载因子.java

  • modCount为修改计数,是fast-fail机制的关键参数.在对Map中的元素作新增/删除操做时会自增,但修改不会(putVal()方法中覆盖原值)git

新增逻辑

  • HashMap的新增过程重点主要仍是定位,如何肯定元素在数组中的位置,HashMap采用的就是Hash算法
    1. 首先HashMap会根据Key的hash值,按照表达式(n - 1) & hash计算出桶的下标
    2. 若是此时桶为空,会建立一个新的Node,做为链表的第一个元素,直接存放在数组中.(之前还据说过什么链表首节点为空的状况,是假的.)
    3. 若是节点存在又会区分树节点(TreeNode)和普通节点(Node)两种状况.
      • 普通节点会直接从首节点往下遍历找到尾节点,并将带插入节点添加到末尾
      • 树节点会调用,TreeNode的方法插入到树中.
  • 另外新增前会判断底层数组table是否初始化,新增后会判断该桶大小是否超过的8,超过则转化为红黑树,再判断整个数组是否须要扩容.
  • Hash同时也叫散列,能够把任意长度的输入经过算法,换算成固定长度的输出,不一样元素经过Hash算法得到的下标一致能够被称之为冲突或者碰撞,Hash算法的要求就是使元素尽可能少的发生碰撞,从而均匀的散布在数组中.而发生碰撞时,像HashMap这种以一个列表下挂的方式能够被称为拉链法.

查找逻辑

  • 此处的查找逻辑是指调用get()方法,经过key值查找的状况,若是本身遍历的另说.
    1. 一样是根据表达式(n - 1) & hash计算出桶的下标(能够说是至关重要了),若获得的桶为空,直接返回null
    2. 不为空时则会遍历整个桶,并根据key.equals(k)判断是否相等
    3. 遍历的方法也会根据节点类型的不一样而不一样,可是区分节点前直接存放在数组中的头结点是要先进行判断的.感受上性能影响不大吧
  • 从查找的过程能够看出,肯定桶下标的计算不存在随机性,时间复杂度就为O(1),具体的性能体如今遍历这一块,链表查询的时间复杂度为O(n),因此链表越长遍历时间也就越长,插入和查找的效率也就越低.因此在JDK1.8以后引入的红黑树做为桶的另外一种实现方法.当链表长度大于8时,桶的实现会转化为红黑树.
  • HashMap的性能很大一部分取决于Hash算法..

RESIZE逻辑

  • 经过插入和查找咱们能够知道,在数组大小不变的状况下,链表越长或者说树的高度越高都会致使操做性能下降,因此此时颇有必要经过扩容数组的方式,从新排列桶中元素,下降链表长度,减小树的高度.github

  • 首先,触发扩容的状况是size > threshold即元素个数大于阈值.整个扩容过程能够简单的拆分为如下几步:算法

    1. 对数组进行扩充,通常状况下是数组容量和阈值都变为原来的两倍,此间会有上限判断,容量最大为1 << 30也就是2^30.
    2. 遍历旧数组,从新判断元素的位置并散布到新数组.
  • resize()方法中从新散布元素的方法仍是颇有意思的(除去单元素链表和红黑树(桶的容量在1~7之间)shell

    • 首先将新数组分为两部分lohi(源码是loHead和hiHead,我猜是low和high,怎么缩写这么随意),lo表示0到旧容量大小部分,hi表示余下算是新加入的部分,并以此建立两个链表的节点
    • 根据表达式e.hash & oldCap判断元素是否分布在lo部分,是就挂到lo链表下面,否就挂到hi链表下面.
    • lo链表挂到和旧数组相同位置的桶,而hi则挂到下标为原下标 + 旧数组容量的桶.
    • 此处的依据就是e.hash & (oldCap - 1) + oldCap == e.hash & (oldCap << 1) -1
  • 能够看出resize()方法会调整所有的元素散列状况,所以过于频繁的resize会下降HashMap的性能,所以若是一开始能够大概知道所须要存放的元素个数时,尽可能直接指定容量大小.数组

  • JDK1.7以前的resize()方法在并发条件下可能会发生闭环问题,但在JDK1.8以后不会在出现,但并不表明HashMap能够在并发条件下使用了,小部分状况仍是会出现数据丢失等问题.安全

  • 介绍JDK1.8以前的闭环问题详情的文章多线程

  • HashMap的懒加载问题并发

    • 查看HashMap的源码,你会发现底层数组table的建立其实并非在构造函数中完成的,而是resize()方法中,这就是所谓的懒加载,数组对象并不是是在一开始就建立的,而是在第一次插入操做以前完成的。

关于HashMap一些问题

扰动函数

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
复制代码

扰动函数的逻辑很简单就是hashCode的高16位和低16位异或.

扰动函数的做用就是增长散列的随机性,使元素可以更均匀的分布在数组中,减小冲突从而捎带提升性能.

至于为何,能够看hash(*)用到的地方,hash(*)被用来计算元素的下标.而下标的计算公式以下

tab[i = (n - 1) & hash]   // n表示数组的长度
复制代码

由于HashMap的容量必定会是2的次幂,因此减1以后转化为二进制会变为一串0加一串1的,例如长度为4时,减去1,就会变为000…00011(前面30个0),再结合&能够发现他只使用了hashCode的末尾几位,高位是所有没用.

而通过扰动函数,将高16位和低16位异或以后至关于高低位都用到了,其散列的随机性也就增长了.

HashMap的容量为何必定要是2的次幂

  • 容量为2次幂有两个优势
    1. 在下标运算的时候使用(length - 1) & hash)代替hash % length,相对来讲位运算性能更佳,速度更快。
    2. 而在采用(length - 1) & hash的方式计算下标以后,若是不是二次幂的容量,出现碰撞的概率将会大大增长,例如咱们取17做为容量((17 -1) => 0001000),通过&与运算,能够想象会有一大批的元素直接挂在0号桶。
  • 能够说这是一整套的策略,若是使用hash & length的话,也不用要求容量必定是二次幂,但各方面的性能老是会差一点的。

HashMap和HashTable的区别

  • HashTable都没用过了,但之前还稍微看过
  1. 最大的区别就是HashTable是线程安全的,暴力的加方法级synchronized.而HashMap是线程不安全的,并发状况下可能会出现数据丢失等状况.
  2. HashTable不容许null值,而HashMap容许null值.(包括key和value)
  3. HashCode的使用不一样,HashTable是直接调用hashCode,而HashMap会通过扰动函数.并且HashMap中用&代替了%
  4. HashTable数组默认是11,且增加为2n+1,而HashMap默认为16,增加为2n,且硬性要求长度为2的次幂.
  5. HashTable并非和HashMap同样继承自AbstractMap的,它继承自一个独立的父类AbstractDictionary
  6. 还有就是遍历方法的不一样.了解不深先不说话.

  • 最后附上完整的源码阅读,蛮久以前写的,不过被朋友吐槽说大段的代码混着注释实在看不下去,因此写了这篇总结性的
相关文章
相关标签/搜索