HashMap的底层实现原理

HashMap的数据结构

HashMap的底层实现原理是根据散列表来实现的,我们可以认为是"链表数组"来实现的;其数据结构如下图所示:

HashMapæ°æ®ç»æå¾_thumb[13]

从上图我们可以看出哈希表是由数组+链表组成的,

数组是什么类型?

当我们在系统中创建hashMap时,系统中定义一个长度默认为16的(Node[])数组,源码如下所示:

transient Node<K,V>[] table;

数组中的元素存储的为链表的头结点Node,其类型如下图所示:

Node(int hash, K key, V value, Node<K,V> next) {
    this.hash = hash; //key对应的hash值
    this.key = key;   //key值
    this.value = value;//value值
    this.next = next; //下一个节点
}

存值put

要存值首先得确定要存储到数组的哪个下标位置,定位到了下标位置,因为数组中保存的是链表的第一个节点,还得判断保存在该位置的链表位置

如何确定元素存储在数组中的下标值?

如果给map中存一个元素,那么是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是该元素的key的哈希值对数组长度取模得到。比如key的哈希值为12则在数组中的下标位置为:12%16=12,28%16=12,108%16=12。所以12、28以及108都存储在数组下标为12的位置,这样表示他们在数组中存储的位置是相同的,也就是产生了碰撞则数组下标为12的位置所存储的链表有三个节点

如何确定链表中的存储位置呢?

已知数组中存储的是每个链表的头结点,链表的每个节点为一个静态内部类Node,其重要的属性有 key ,value, next,用来存储map中的键与值还有指向的下一个节点;代码如下所示:

public V put(K key, V value) {

        if (key == null)

            return putForNullKey(value); //null总是放在数组的第一个链表中

        int hash = hash(key.hashCode());//获取key值得hash值

        int i = indexFor(hash, table.length);//根据hash值就可以定位到数组的下标位置

        //遍历数组此位置的链表

        for (Entry<K,V> e = table[i]; e != null; e = e.next) {

            Object k;

            //如果key在链表中已存在,则替换为新value

            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                return oldValue;

            }

        }

        modCount++;

        addEntry(hash, key, value, i);

        return null;

    }

void addEntry(int hash, K key, V value, int bucketIndex) {

    Entry<K,V> e = table[bucketIndex];//获取到原来第一个节点

    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next指向那个节点

    //如果size超过threshold,则扩充table大小。再散列

    if (size++ >= threshold)

            resize(2 * table.length);

}

根据key的hash确定在数组中的下标位置;然后获取数组中该下标位置的所有链表节点并且循环遍历,判断每个节点的key值与当前key值是否相等,如果相等则替换掉旧值;如果不存在相等的key值,则将该key与value存储到新的Node中并且保存到链表的第一个位置,next指向之前旧的Node;也就是说数组中存储的是最后插入的元素.打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Node[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Node[0] = B

到这里为止,HashMap的大致实现,我们应该已经清楚

取值get

代码如下所示:

public V get(Object key) {
        // 若为null,调用getForNullKey方法返回相对应的value
        if (key == null)
            return getForNullKey();
        // 根据该 key 的 hashCode 值计算它的 hash 码  
        int hash = hash(key.hashCode());
        // 取出 table 数组中指定索引处的值
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            //若搜索的key与查找的key相同,则返回相对应的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

取值的时候,如果传的key值为null则直接调用查询空方法getForNullKey,此处不多追,如果key不为null则根据key值获取对应的hashcode从而定位到数组中的下标位置;然后循环遍历数组该位置的链表节点,如果key值相等,则返回对应的value,否则返回null

负载因子

当我们在代码中创建一个hashMap 如下图所示:

HashMap<Object,Object> hashMap=new HashMap<>()
默认调用构造方法 HashMap(int initialCapacity, float loadFactor)其中initialCapacity为默认数组大小为16,loadFactor为负载因子默认为0.75

负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费;源码中默认负载因子为0.75如图所示:

扩容

 随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*负载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值