HashMap实现原理

 HashMap是基于哈希表的Map接口的非同步实现,也就是说不是线程安全的。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

首先我们通过一个图,来从视觉上感受一下HashMap的结构。

从上图中可以看出,HashMap的底层结构是一个数组结构,数组中的每一项又是一个单项的链表结构,当我们通过构造函数初始化一个HashMap的时候,就会初始化一个数组。

1. 下面分析一下HashMap的属性:

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table; //这里很明显表明了HashMap的底层是一个Node<K,V>类型的数组

/**
 * Holds cached entrySet(). Note that AbstractMap fields are used
 * for keySet() and values().
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * The number of key-value mappings contained in this map.
 */
transient int size; //实际存储的key-value键值对的个数

/**
 * The number of times this HashMap has been structurally modified
 * Structural modifications are those that change the number of mappings in
 * the HashMap or otherwise modify its internal structure (e.g.,
 * rehash).  This field is used to make iterators on Collection-views of
 * the HashMap fail-fast.  (See ConcurrentModificationException).
 */
transient int modCount; //用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException

/**
 * The next size value at which to resize (capacity * load factor).
 *
 * @serial
 */
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold; //阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到

/**
 * The load factor for the hash table.
 *
 * @serial
 */
final float loadFactor;  //负载因子,代表了table的填充度有多少,默认是0.75
/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node<K,V> implements Map.Entry<K,V> { //HashMap底层的链表结构
    final int hash; //对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算,这个属性很重要
    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的结构是下面这样的:

 

2. 分析HashMap相关的数据操作方法:

2.1 put方法

public V put(K key, V value) {
      // HashMap允许存放null键和null值。
      // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
     if (key == null)
          return putForNullKey(value);
      // 根据key的keyCode重新计算hash值。
      int hash = hash(key.hashCode());
      // 搜索指定hash值在对应table中的索引。
      //key的hash值一样的链表
      int i = indexFor(hash, table.length);
     // 如果 i 索引处的 链表Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             //这句话很重要,为了比较key是否相等,先通过比较两者的hashcode是否相等,如果hashcode都不相等,那么两者的key肯              定不相等,这样做的目的是为了提高比较的效率,如果hashcode相等,接着再进行比较,如果最后相等,那么使用新的Node<k,v>节点覆盖掉当前key所在的链表节点数据
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             return oldValue;
         }
     }
     // 如果i索引处的Entry为null,表明此处还没有Entry。
     modCount++;
     // 将key、value添加到i索引处。
     addEntry(hash, key, value, i);
     return null;
 }

根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。

static int hash(int h) {
   h ^= (h >>> 20) ^ (h >>> 12);

 return h ^ (h >>> 7) ^ (h >>> 4);
 }
我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。

根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

通过这种方式就可以高效的解决HashMap的冲突问题。

2.2 get方法

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值,找到数组table中对应位置的某一元素(可能是链表也有可能是节点),然后通过key的equals方法在对应位置的链表中找到key对应的value值。

HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

总结一下HashMap的大致实现原理:

1. 利用key的hashCode重新hash计算出当前对象的元素在数组中的位置
2. 在往HashMap中存放数据时,如果出现hash值相同的key,那么有两种处理情况:

    2.1 如果key相同,则覆盖原始值;即覆盖掉value的值

    2.2 如果key不同(出现冲突),则将当前的key-value节点放入当前链表中


3. 获取时,直接通过key的hash值找到在数组中的下标获取到当前下标对应的链表,然后再进一步循环这个链表取得每一个节点判断节点的key和当前要获取的数据的key是否相同,从而找到对应值。
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值