Java | HashMap

Java8 HashMap 内部主要特点:

使用哈希表(散列表)来进行数据存储,并使用链地址法来解决冲突;

当链表长度大于等于 8 时,将链表转换为红黑树来存储;

每次进行二次幂的扩容,即扩容为原容量的两倍;

再来看下Java8 HashMap中的部分代码具体实现:

静态属性初始化赋值 :

// 静态属性初始化赋值 
// << 左移位运算,将运算对象的二进制位全部左移若干位(左边的二进制位丢弃,右边补0)
// 若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
static final int MAXIMUM_CAPACITY = 1 << 30; 
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
// 指定树化的最小表容量为 64
static final int MIN_TREEIFY_CAPACITY = 64;

定义变量:

// 定义的变量
// 存储数据的哈希表;初始长度 length = DEFAULT_INITIAL_CAPACITY = 16,扩容时容量为原先的两倍(n * 2),确保长度总是2的幂。
transient Node<K,V>[] table;
// 当前 key-value 个数
transient int size;
// HashMap 结构修改次数(eg:每次 put 新值使则自增 1)
transient int modCount;
// 所能容纳的 key-value 对, threshold = length * Load factor,当存在的键值对大于该值,则进行扩容 
int threshold;
// 负载因子,确定数组长度与当前所能存储的键值对最大值的关系;不建议轻易修改,除非情况特殊
final float loadFactor;

初始化HashMap,构造方法:

// 赋值 loadFactor = DEFAULT_LOAD_FACTOR = 0.75f 
public HashMap() {
  this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

方法:put() 

// put()是 HashMap中涉及到数据存储的方法,其它的还有putAll()等。
public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

put() 方法中调用了 putVal() 与hash()  ,先看下hash(), 如下:

// 通过hash()可以看出 key可以为空,但hash取0 。
// key不为空时则取key的 hashCode()值 然后逻辑右移16位参与异或运算。逻辑右移16位再异或运算即高16位与低16位进行异或,而原高16位不变。
// 这么做主要用于当HashMap数组比较小的时候所有Bit都参与运算,防止Hash冲突太大。
// Hash冲突指不同的key计算出的hash值是一样的。例如String i = "a" 与 Integer j = 97。 此时i与j的hash值就是一样的。
static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

再来看看putVal() ,注意这里是第一次初始化中初次调用putVal()过程:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  // 申明变量 注意:Node 是内部的一个静态类,内部申明了hash、key、value和另一个Node对象引用的HashMap子元素结构,即每次装的每个键值对就用一个Node对象存放
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // tab = table赋值,table现在是null的,因此n = tab.length不运行,开始执行该if代码块
  if ((tab = table) == null || (n = tab.length) == 0)
    // 调用resize(),返回Node数组,得到数组n长度值。n = 16 
    n = (tab = resize()).length;
  // 得到数组长度n=16后,则计算 i = 15&hash; 因为15二进制 = 1111,因此涉及到&与运算时,i的值总在0-15之间,即i=0,1,2,...,15;
  // 因此i的值实际上就是在计算一个key在整个数据结构tab且大小为16的数组中的索引值。
  // tab通过resize()得到一个初始化数组,但并没有值。因此执行该if代码块
  if ((p = tab[i = (n - 1) & hash]) == null)
    // 实际上是new Node<>(hash, key, value, next); 并将其放在数组tab中,其索引值为上一句计算获取的i值。
    tab[i] = newNode(hash, key, value, null);
  else {
    // 此时为第一次调用初始化过程,该else代码块不执行。
    Node<K,V> e; K k;
    if (p.hash == hash &&
      ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    else if (p instanceof TreeNode)
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        if (e.hash == hash &&
          ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  // 记录数据结构变动次数。
  ++modCount;
  // threshold 在resize()内部方法中赋值为12,size = 1,因此该代码块不执行。
  if (++size > threshold)
    resize();
  //  貌似啥事没干
  afterNodeInsertion(evict);
  return null;
}

初始化初次调用putVal()时,该方法内部会初次调用resize(),如下

if ((tab = table) == null || (n = tab.length) == 0)
    // 调用resize(),返回Node数组,得到数组n长度值。n = 16
    n = (tab = resize()).length;

而初次调用resize()详细过程如下:

final Node<K,V>[] resize() {
  // 此时table == null,则oldTab == null 
  Node<K,V>[] oldTab = table;  
  // 得到oldCap = 0 
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  // 此时threshold 未赋值,因此 oldThr = threshold = 0
  int oldThr = threshold;
  int newCap, newThr = 0;
  // if 不满足, else if 也不满足 ,第一次初始化过程默认执行else代码块
  if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
         oldCap >= DEFAULT_INITIAL_CAPACITY)
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
  else {               // zero initial threshold signifies using defaults
    // 赋值newCap = 默认初始容量大小 = 1 << 4 = 16 
    newCap = DEFAULT_INITIAL_CAPACITY;
    // 赋值newThr = 负载因子 * 默认初始容量 = 0.75 * 16 = 12 
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  // 此时 newThr = 12,该if代码块不执行
  if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
          (int)ft : Integer.MAX_VALUE);
  }
  // 第一次赋值 threshold = newThr = 12 
  threshold = newThr;
  // 申明一个Node数组,且数组大小为 newCap = 16 
  @SuppressWarnings({"rawtypes","unchecked"})
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  // 将newTab赋值给table,注意table是HashMap的成员变量,因此表名HashMap初始化数据结构是一个Node数组,且大小为16
  table = newTab;
  // 此时oldTable为第一行赋值的 table,因此oldTable == null ,该if代码块不执行。
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      Node<K,V> e;
      if ((e = oldTab[j]) != null) {
        oldTab[j] = null;
        if (e.next == null)
          newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode)
          ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // preserve order
          Node<K,V> loHead = null, loTail = null;
          Node<K,V> hiHead = null, hiTail = null;
          Node<K,V> next;
          do {
            next = e.next;
            if ((e.hash & oldCap) == 0) {
              if (loTail == null)
                loHead = e;
              else
                loTail.next = e;
              loTail = e;
            }
            else {
              if (hiTail == null)
                hiHead = e;
              else
                hiTail.next = e;
              hiTail = e;
            }
          } while ((e = next) != null);
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
  // 初次即第一次调用resize方法,默认直接返回基本数据结构Node,且大小为16. 注意此时初次调用resize()时,方法内部的 if (oldTab != null) {.....}这个代码块还未执行。
  return newTab;
}

通过上述代码可以看到初次调用putVal()时,很多代码块都还没执行,那么再回头看下初始化HashMap完后,假设第二次执行putVal(),又会执行哪些代码块,过程如下:

// 再回头来看看putVal(),注意 非第一次初始化过程 非第一次初始化过程
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
         boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // 注意此时tab = table,table在第一次初始化过程中初次调用resize()时,已经初始化为数组大小为16的数组结构。因此该if代码块不执行
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  // 判断当前存的key计算出的索引位置是不是已经存过值。如果没则继续new Node()存值。跟第一次初始化过程一样。
  // 如果有值,则说明发生了hash值冲突。例如 String i = "a" 与 Integer j = 97。此时i与j的hash值就冲突了。
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else {
    // 该else代码块中主要处理Hash冲突
    Node<K,V> e; K k;
    // p 是hash冲突时该索引下的数组元素
    // 判断新元素hash和key是不是都和p相同,相同表示存的key一样
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
      // 赋值e 
      e = p;
    // 判断元素p是不是TreeNode节点,若是则将元素添加到红黑树中。注意TreeNode是HashMap中的静态内部类,也是传闻在此用到了红黑树
    else if (p instanceof TreeNode)
      // putTreeVal 往红黑树中添加元素,存在相同key就返回赋值给e,不存在就添加并返回null
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      // 从头部遍历链表 
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          // 将新元素挂载到链表尾部
          p.next = newNode(hash, key, value, null);
          // 当链表中元素阈值时,这时有可能将链表改造为红黑树的数据结构
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        if (e.hash == hash &&
          ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    // 判断e是否为空,为空则表示无相同key且完成了数据挂载
    // e非空则表示有相同的key,则进行value覆盖。
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  // 记录数据结构变动次数。
  ++modCount;
  // 当size大小超过阈值threshold时,则重新扩容
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

此时再来看下 resize()方法又做了什么操作,过程如下:

// threshold代表HashMap的容量,默认16数组,12元素上限。threshold初始12,当++size>12时,则达到上限而需要扩容。
 final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table;
  // 此时table非空,则oldCap = 16 默认16.
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  // 默认容量上限 oldThr = threshold = 12
  int oldThr = threshold;
  int newCap, newThr = 0;
  if (oldCap > 0) {
    // 满足oldCap = 16 > 0 这个条件,然后判断数组大小是否已经到达上限 1 << 30
    if (oldCap >= MAXIMUM_CAPACITY) {
      // 若达到上限则赋值最大值,然后返回oldTab。
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }
    // 先赋值newCap为oldCap的2倍大小,然后判断扩充2倍后是否达到上限且不扩充时即oldCap容量是否大于默认大小16.
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
         oldCap >= DEFAULT_INITIAL_CAPACITY)
      // 若满足则赋值,将新的容量扩充为oldThr的2倍大小。
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
  else {               // zero initial threshold signifies using defaults
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  // 若运行该if代码块,则newThr == 0 表示 上述if oldCap>0代码块中扩容2倍到达上限 
  if (newThr == 0) {
    // newCap为2倍原数组大小 * 0.75 
    float ft = (float)newCap * loadFactor;
    // 判断扩容2倍后数组大小和2倍后容量大小是不是都小于最大容量上限,如果是则赋值新容量,否则赋予整形最大值。
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
          (int)ft : Integer.MAX_VALUE);
  }
  // 将新容量大小赋值给threshold,此时HashMap的容量即为threshold;
  threshold = newThr;
  // 数组扩容后,将原数组元素一个个添加到新数据中。
  @SuppressWarnings({"rawtypes","unchecked"})
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  table = newTab;
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      Node<K,V> e;
      if ((e = oldTab[j]) != null) {
        oldTab[j] = null;
        if (e.next == null)
          newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode)
          ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // preserve order
          Node<K,V> loHead = null, loTail = null;
          Node<K,V> hiHead = null, hiTail = null;
          Node<K,V> next;
          do {
            next = e.next;
            if ((e.hash & oldCap) == 0) {
              if (loTail == null)
                loHead = e;
              else
                loTail.next = e;
              loTail = e;
            }
            else {
              if (hiTail == null)
                hiHead = e;
              else
                hiTail.next = e;
              hiTail = e;
            }
          } while ((e = next) != null);
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
  return newTab;
}

至此通过HashMap中put()方法知道了基本结构是数组、链表、红黑树,链表到8个时转换成红黑树,同时每次进行2倍扩容和数据转移,扩容是用新结构的那显然减少扩容次数会有更好的性能那就要求每次声明HashMap时最好是指定大小。


再来看看HashMap中其它方法:

treeifyBin() :树化过程如下:

// 再来看下树化过程:
final void treeifyBin(Node<K,V>[] tab, int hash) {
  int n, index; Node<K,V> e;
  // 之前有说当链表中元素大于等于 8,这时有可能将链表改造为红黑树的数据结构。
  // 当 此时条件为true时,则继续进行扩容处理,而没有树化。
  if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();
  else if ((e = tab[index = (n - 1) & hash]) != null) {
    TreeNode<K,V> hd = null, tl = null;
    do {
      TreeNode<K,V> p = replacementTreeNode(e, null);
      if (tl == null)
        hd = p;
      else {
        p.prev = tl;
        tl.next = p;
      }
      tl = p;
    } while ((e = e.next) != null);
    if ((tab[index] = hd) != null)
      hd.treeify(tab);
  }
}

get() : 获取元素过程如下:

    哈希表是否为空或者目标位置是否存在元素;

    是否为第一个元素;

    如果是树节点,寻找目标树节点;

    如果是链表结点,遍历链表寻找目标结点;

public V get(Object key) {
  Node<K,V> e;
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}
  
final Node<K,V> getNode(int hash, Object key) {
  Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
    // always check first node
    // 检查当前位置的第一个元素,如果正好是该元素,则直接返回
    if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
      return first;
    // 如果不是则检查下一个元素 
    if ((e = first.next) != null) {
      //  判断是否为树节点,则调用 getTreeNode 方法获取树节点
      if (first instanceof TreeNode)
        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
      // 遍历整个链表,寻找目标元素
      do {
        if (e.hash == hash &&
          ((k = e.key) == key || (key != null && key.equals(k))))
          return e;
      } while ((e = e.next) != null);
    }
  }
  return null;
}
发布了44 篇原创文章 · 获赞 11 · 访问量 5442

猜你喜欢

转载自blog.csdn.net/Sampson_Hugo/article/details/103524155