HashMap源码解析

(1)HashMap概述

             HashMap是基于哈希表的Map接口的非同步实现。允许使用null值和null键。此类不保证映射的顺序,特别是他不保证该顺序恒久不变。

(2)HashMap数据结构

             HashMap实际上是一个链表散列的结构,即数组和链表的结合体,实际上就是Hash表通过链表来解决冲突。

              

            从图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表,当新建一个HashMap的时候,就会初始化一个数组。

这个数组的结构如下:

    transient Entry<K,V>[] table;
    
    static class Entry<K,V> implements Map.Entry<K,V> 
    {
        final K key;
        V value;
        Entry<K,V> next;
    }
          可以看出,Entry就是数组中的元素,每个Entry实际上就是一个key-value对,并且可以指向链表下一个节点,这就构成了链表。

(3)HashMap存储

    public V put(K key, V value) 
    {
                                                       // HashMap允许存放null键和null值。
        if (key == null)                              // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
            return putForNullKey(value);
        
        int hash = hash(key.hashCode());              // 根据key的keyCode重新计算hash值。
        
        int i = indexFor(hash, table.length);         // 获得指定hash值在对应table中的下标。
        
        for (Entry<K,V> e = table[i]; e != null; e = e.next)      //循环table[i]的链表(该链表中所有)
        {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  //如果该值已经存在,那么就替换
            {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        
        modCount++;                      // 如果i索引处的Entry为null,表明此处还没有Entry。那么就总数+1
       
        addEntry(hash, key, value, i);  // 将key、value添加到i索引处。
        return null;
    }
            可以看出,当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值:

   static int hash(int h) 
    {  
        h ^= (h >>> 20) ^ (h >>> 12);  
        return h ^ (h >>> 7) ^ (h >>> 4);  
    }
           再根据hash值得到这个元素在数组中的下标,如果数组该位置上已经有其他元素了,那么在这个位置上的元素将以链表的形式存放,如果其中已经有key相同的,那么就直接替换value;如果没有key相同的,那么添加到链表中;如果数组该位置上没有元素,那么直接将该元素放置到数组中的该位置上。

           addEntry(hash, key, value, i)方法根据计算出的hash值,将key-value放在数组table的下标i处(不管原来i位置是否为null,都是这种方法)

    void addEntry(int hash, K key, V value, int arrayIndex) 
    {
        Entry<K,V> e = table[arrayIndex];                     // 获取指定 arrayIndex 索引处的 Entry 
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);       // 将新创建的 Entry 放入 arrayIndex 索引处,并让新的 Entry 指向原来的 Entry
        if (size++ >= threshold)                              // 如果 Map 中的 key-value 对的数量超过了极限
            resize(2 * table.length);                         // 把 table 对象的长度扩充到原来的2倍。
    }

        根据hashCode可以找出key在table中的位置,一般的思路就是对长度取模,但是取模运算的时间消耗还是比较大的,所以HashMap使用indexFor(int h, int length)方法来计算该key在table中的下标:

    static int indexFor(int h, int length) 
    {
        return h & (length-1);
    }
         这个方法通过h&(table.length-1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。在HashMap构造方法中:

        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
       这段代码保证初始化时HashMap的容量总是2的n次方,即底层数组的长度总是2的n次方。 当length总是2的n次方时,h&(length-1)等价于h%length,但是效率更高

(4)HashMap读取

    public V get(Object key) 
    {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
              从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素


(5)HashMap的扩容

              当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越多,因为数组的长度是固定的。所以为了调高查询效率,就要对HashMap的数组进行扩容,而在扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize

             那么HashMap何时进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值时0.75,这是一个折中的取值。也就是说,在默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12时,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每一个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap的元素个数,那么预设元素的个数能够有效提高HashMap的性能。

           负载因子是衡量一个散列表空间的使用程度,定义为:实际元素数目(n)/散列表的容量(m)。负载因子越大表示散列表的装填程度越高,反之越小。对于使用链表法的散列表来说,查找一个元素的平均时间为O(1+a),如果负载因子越大,对空间的利用更充分,然而后果就是冲突增加,查询效率变低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

          在HashMap中,通过threshold来判断HashMap的最大容量,超过这个数目,就重新resize,以降低实际的负载因子。

 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值