HashMap浅析

HashMap浅析

简单总结一下最近看的HashMap相关知识点,前面部分为JDK7中的处理流程,JDK8中的处理方式会在JDK7和JDK8的区别中进行介绍

  • 哈希冲突的解决方法
  • HashMap的实现原理
  • JDK7和JDK8的区别

哈希冲突的解决方法

  • 开放地址法
    • 当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。对于开放地址法还存在不同的方法,有线性探测再散列、二次探测再散列、伪随机再散列。
    • 这种称法是针对于再次探测位置的增量来说的
      ( 1 ) d i = 1 , 2 , 3 , …… 线性探测再散列;
      ( 2 ) d i = 1^2 ,- 1^2 , 2^2 ,- 2^2 , k^2, -k^2…… 二次探测再散列;
      ( 3 ) d i = 伪随机序列 伪随机再散列;
    • 对于使用该方法的Hash表来说,在删除方法中不能真正将数据删除,而需要设置标志位或者特殊数据将该位置标识为空,直接删除会影响相同hash值的元素进行查找
  • 链地址法
  • 再散列函数法

HashMap的实现原理

Entry

HashMap的主干是一个Entry数组,每个Entry是一个键值对

//Entry是HashMap中的静态内部类,在HashMap中称为Node
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

HashMap是由数组+链表组成的,数组是HashMap的主体,链表是为了解决Hash冲突而存在的


几个重要的字段

  1. size:实际存储的键值对个数
  2. loadFactor:负载因子(默认为0.75)代表当table的填充度到达负载因子时需要对table进行扩容
  3. threshold(阈值):当table={}时,capcity(容量)默认为16,当table被填充了,也就是为table分配内存后,threshold=capacity*loadFactor,当size>threshold时,需要对table进行扩容
  4. modCount:用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生了变化(比如put和remove操作),需要抛出ConcurrentModificationException异常

HashMap的四个构造器

  1. 传入initialCapacity和loadFactor
  2. 传入initialCapacity
  3. 无参数
  4. 传入Map

1.如果没有传入initialCapacity和loadFactor,会使用默认值16和0.75。初始容量不能超过1<<30(2^30)
2.在常规构造器中,没有为table分配内存空间,除了入参为Map的构造器,而是在执行put操作的时候才真正构建table数组

put操作

//如果table数组为空数组,进行数组填充(为table分配实际的内存空间) 
---inflateTable
//如果key为null,存储位置为table[0]或者table[0]的冲突链上
//对key的hashcode进一步hash,确保散列的
//获取key再table中的实际位置
//如果该位置数据已存在执行覆盖操作,用新value替换旧value,并返回旧value
//保证并发访问时,若hashmap内部结构发生变化,快速相应失败
//新增一个entry

inflateTable

//capacity一定是2的次幂---roundUpToPowerOf2(tosize)
//此次threhold赋值取min(capacity*loadFactor,MAXIMUM_CAPACITY+1),threshold一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
//table = new Entry[capacity];
//initHashSeedAsNeeded(capacity);

inflateTable用于为数组table分配内存空间,通过roundUpToPowerOf2(toSize)可以确保capacity为最接近toSize的二次幂。

roundUpToPowerOf2

return number>=MAXIMUM_CAPACITY
            ?MAXIMUM_CAPACITY
            :(number > 1)?Integer.highestOneBit((number-1)<<1):1

roundUpToPowerOf2使得数组长度一定为2的次幂,Integer.highestOneBit是用来获取最左边的bit,其他位的bit为0所代表的值

indexFor

处理来获取实际的存储位置hash&(table.length-1)
indexFor(hash,table.length)
保证获取的index一定在数组范围内
有些版本对于此处的计算会使用取模运算,但位运算的效率高

存储位置的确定流程

1.key->hashcode:调用hashCode()方法
2. hashCode->h:调用hash()方法,异或或移位等运算,对key的hashCode进一步计算以及二进制位的调整等来保证最终获取的index尽量分布均匀
3. 调用indexFor方法获取存储下标

在JDK7中,第二步存在一个变量hashSeed,可以把它看成一个开关打开,并且key的类型是String时可以采取sun.misc.Hashing.stringHash32方法获取其hash值,hashSeed默认是0,会在capacity发生变化时调用initHashSeedAsNeeded方法重新计算

addEntry

当发生hash冲突并且size>threshold时会对数组进行扩容,扩容时会新建一个长度为之前两倍的新数组,然后将当前的Entry数组中的元素全部传输过去。

resize

IF capacity == MAXIMUM_CAPACITY
   threshold = MAXIMUN_INTEGER
ELSE
   //创建新的数组,长度为新容量的长度
   //将数据转移到新数组 --- transfer(newTable.initHashSeedAsNeeded(newCapacity))
   //阈值等于min(newCapacity*loadFactor,MAXIMUM_CAPACITY+1)

transfer

如果数组进行扩容,数组长度将发生变化,存储位置index=h&(length-1)也可能发生变化,需要重新计算index

//for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数据
    //需要判断hashSeed是否改变,改变需要重新计算hash
    //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,也有可能是个entry链,如果是entry链,直接在链表头部插入

hashMap的数组长度一定为2的次幂

  1. 当数组长度为2的次幂时,h & (length - 1) == h % length,而且位运算快
  2. 使得获得的数组索引index更加均匀, &运算,高位是不会对结果产生影响的(hash函数采用各种位运算也是为了使得地位更加散列),length-1低位为1,对于h低位部分任何一位的变化都会对结果产生影响,也就是要得到某个存储位置的h低位只有一种组合
  3. 如果不是2的次幂,会存在位置不能存放元素,空间浪费相当大,更糟的是,数组可以使用的位置比数组长度小很多,增加了碰撞记录,减慢查询效率

get

get方法通过key值返回对应value,如果key为null,直接在table[0]处查询,否则调用getEntry(key)方法

getEntry

//如果size==0,返回null
//通过key的hashCode值计算hash值
//通过indexFor(hash&(length-1))获取最终数组索引,然后遍历链表,通过equals方法比对找出相应记录

key.hashCode()->hash->indexFor->最终索引位置,找到对应位置table[i],再看是否有链表,遍历链表,通过key的equals方法对比查找对应的记录

注:在遍历链表时,e.hash == hash这个判断有没有必要?
如果传入的对象重写equals方法,但没有重写hashCode方法,而恰巧此对象定位到这个数组位置,,如果仅仅用equals判断可能是相等的,但是hashcode却不一样,这种情况,根据Object的要求,不能返回当前对象,而是应该返回null


JDK7和JDK8的区别

JDK中的HashMap采用链地址法来处理冲突

JDK7中,HashMap的数据结构是数组+链表<
JDK8中,HashMap的数据结构是数组+链表或者数组+红黑树(rbtree)

JDK8移除hashSeed

Hashing.randomHashSeed计算hashSeed时会调用方法java.util.Random. nextInt(),该方法使用AtomicLong,在多线程中会有性能问题

hash函数

JDK8中在进行get和put操作时,会现根据key的hashCode进行再散列,再进行桶(bucket)对应节点位置计算

例: h=key.hashCode: 1111 0101 1000 1001 1111 0100 0001 1010
h>>>16: 0000 0000 0000 0000 1111 0101 1000 1001
hash=h^(h>>>16): 1111 0101 1000 1001 0000 0001 1001 0011
n-1: 0000 0000 0000 0000 0000 0000 0000 1111
index=hash&(n-1): 0000 0000 0000 0000 0000 0000 0000 0011

h>>>16,高16位补0,由于任意数据跟0异或不变,所以hash的作用就是高16位不变,低16位和高16位做异或运算,来达到减少碰撞的目的

//hash方法的具体实现
static final int hash(Object key){
    int h;
    return key==null?0:(h=key.hashCode())^(h>>>16);
}

rbtree

为了提高碰撞下的性能,JDK8引入了rbtree来代替链表,将原有链表部分查询的时间复杂度O(n)提升为O(logn)

JDK8中的put方法
    put方法中的所有操作都在putVal中实现

putVal方法
    1.如果当前bucket为空时(capacity),调用resize进行初始化
    2.根据key的hash值计算出所在bucket节点位置
    3.如果没有发生冲突,调用newNode方法封装key-value键值对,并将其存储到对应bucket位置下,否则跳转到步骤4
    4.如果发生冲突
        如果该key已经存在,更新原有oldValue为新的value,并返回oldValue
        如果key所在的节点为treeNode,调用rbtree的putTreeVal方法将该节点存储到rbtree中
        如果插入节点后,当前bucket节点下链表长度超过8,需要将数据结构变为rbtree
    5.数据put完成之后,如果当前长度大于threshold,调用resize方法扩容

resize实现

resize的前半部分主要完成了新的capacity和threshold的计算,从代码实现,newCapacity和newThreshold均时扩容前的两倍

rehash解释

当hash表中的负载因子达到负载极限的时候,hash表会自动成倍地增加容量,并将原有对象重新分配并加入新的桶中,这称为rehash

注:原因可参照为什么容量值为2的次幂

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值