Java程序员从笨鸟到菜鸟(二十八)HashMap实现原理

一、HashMap

HashMap继承自AbstractMap,AbstractMap是Map接口的骨干实现,AbstractMap中实现了Map中最重要且最常用的方法,这样HashMap继承AbstractMap就不需要实现Map所有的方法

HashMap的成员变量

  • initalCapacity:默认初始容量为 16
  • maxCapacity:最大容量为2^30
  • loadFactor:默认加载因子为 0.75f
  • Entry<K,V>[] table:Entry类型的数组,HashMap用这个来维护内部的数据结构,它的长度由容量决定
  • size:HashMap的大小
  • threshold:HashMap的极限容量,扩容临界点(容量和加载因子的乘积)

HashMap的构造函数

public HashMap(); 初始默认容量(16)和默认加载因子(0.75)
public HashMap(int initialCapacity); 指定初始容量和默认加载因子(0.75)
public HashMap(int initialCapacity, float loadFactor); 指定初始容量和加载因子
public HashMap(Map<? extends K, ? extends V>m);构造一个映射关系与指定Map相同的HashMap

HashMap的实例两个参数影响性能:初始容量默认加载因子

初始容量:是哈希表中桶的数量,初始容量只是哈希表在创建时的容量

加载因子:是哈希表在其容量自增前可以达到多满的一种尺度,衡量的是一个散列表的空间使用程度,加载因子越大表示散列表的装填程度越高,但是查找效率低下;如果负载因子太小,那么散列表的数据过于稀疏,对空间造成浪费,系统默认加载因子是0.75,大多数情况下是不需要更改的
当哈希表中的数据条数超出了加载因子与当前容量的乘积时,会进行扩容,从而哈希表将具有大约两倍的桶数

二、HashMap的数据结构

HashMap的底层实现还是数组,只是数组的每一项都是一条链

构造函数的源码

public HashMap(int initialCapacity, float loadFactor) {
    // 容量不能小于0
    if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 容量不能超出最大值                                       
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 加载因子不能小于等于0 或者为非数字
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // 计算出初始容量的大小 2的n次方为哈希表table的长度
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        // 设置哈希表的容量极限,当达到极限时就会进行扩容操作
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 创建Entry数组
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();

}

构造函数主要处理的工作:

  1. 对传入的容量和加载因子进行判断并处理
  2. 设置HashMap的极限容量
  3. 计算出初始容量的最小2^n次方作为哈希表的长度,然后使用该长度创建Entry数组

Entry元素的内部结构

static class Entry<K,V> implements Map.Entry<K,V> {
     final K key;
     V value;
     Entry<K,V> next;
     int hash;

     /**
      * Creates new entry.
      */
     Entry(int h, K k, V v, Entry<K,V> n) {
         value = v;
         next = n;
         key = k;
         hash = h;
     }

Entry是HashMap的一个内部类,维护着Key-value映射关系,除了key和value,还有next引用(该引用指向当前table位置的链表),hash值(用来确定每一个Entry链表在table中位置)

三、HashMap的存储实现

put(K, V)方法源码

public V put(K key, V value) {
      // 判断key值为空的情况
      if (key == null)
          return putForNullKey(value);
      // 计算key值的hash值
      int hash = hash(key);
      // 计算Hash值在table中的下标
      int i = indexFor(hash, table.length);
      // 对table[i]存放的链表进行遍历
      for (Entry<K,V> e = table[i]; e != null; e = e.next) {
          Object k;
          // 判断链上是否有相同的hash值(也就是值key值)
          if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
              // 存在相同的hash值,直接覆盖value返回旧的value
              V oldValue = e.value;
              e.value = value;
              e.recordAccess(this);
              return oldValue;
          }
      }
      // 修改次数自增长
      modCount++;
      // 把当前key, value添加到table[i]中
      addEntry(hash, key, value, i);
      return null;
  }

源码分析

  1. 如果Key值为null,则调用putForNullKey:这就是HashMap可以用null作为键的原因
putForNullKey(V value)源码
private V putForNullKey(V value) {
      // 查找表中是否有null键
      for (Entry<K,V> e = table[0]; e != null; e = e.next) {
          if (e.key == null) {
              V oldValue = e.value;
              e.value = value;
              e.recordAccess(this);
              return oldValue;
          }
      }
      modCount++;
      // 如果链中查不到,则把该null键插入
      addEntry(0, null, value, 0);
      return null;
  }
addEntry(int hash, K key, V value, int bucketIndex)源码
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            // 对null值的处理,如果key为null,hash值为0,也就是会插入到table[0]的位置
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
  1. 如果链表中存在该Key,则用传入的value覆盖旧的value,再把旧的value返回,HashMap中不能有相同的key值

对于hash操作,需要确定hash的位置
首先求得key的hash值:has(key)

final int hash(Object k) {
      int h = 0;
      if (useAltHashing) {
          if (k instanceof String) {
              return sun.misc.Hashing.stringHash32((String) k);
          }
          h = hashSeed;
      }

      h ^= k.hashCode();

      // This function ensures that hashCodes that differ only by
      // constant multiples at each bit position have a bounded
      // number of collisions (approximately 8 at default load factor).
      h ^= (h >>> 20) ^ (h >>> 12);
      return h ^ (h >>> 7) ^ (h >>> 4);
  }

计算hash值在table的下标

static int indexFor(int h, int length) {
    return h & (length-1);
}

将key-value插入到该索引的链表中

addEntry(int hash, K key, V value, int bucketIndex)源码
void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果size大于极限容量,将要进行重建内部数据结构操作,之后的容量是原来的两倍,并且重新设置hash值和hash值在table中的索引值
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //真正创建Entry节点的操作
        createEntry(hash, key, value, bucketIndex);
    }
void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

存储的步骤:

  1. 传入key和value,判断value是否为null,如果为null,则调用putForNullKey,以null作为key存储到哈希表中
  2. 计算key的hash值,根据hash值搜索在哈希表中table中的索引位置, 若当前索引位置不为null,则对该位置的Entry链表进行遍历,如果链中存在该key,则用传入的value覆盖掉旧的value,同时把旧的value返回
  3. 否则调用addEntry,用key-value创建一个新的节点,并把该节点插入到该索引对应的链表的头部

四、HashMap的遍历

entrySetkeySet两种方式遍历,以及两种方式的优劣势比较,详见另一篇博客:https://blog.csdn.net/u013090299/article/details/80495782

猜你喜欢

转载自blog.csdn.net/u013090299/article/details/80626075
今日推荐