HashMap-resize重定位

爷,独闯天下 提交于 2020-03-03 16:58:53

​常量

 // 默认初始化容量 16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量static final int MAXIMUM_CAPACITY = 1 << 30;// 默认加载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链 转 tree 的 节点个数 下限阈值static final int TREEIFY_THRESHOLD = 8;// tree 转 链 的 节点个数 上限阈值static final int UNTREEIFY_THRESHOLD = 6;// 链 转 tree 时 存储数组table的容量下限阈值. table.length小于此值时 resize()扩容。static final int MIN_TREEIFY_CAPACITY = 64;

变量

// Node存储数组,在resize方法中初始化或扩容. 长度一定是 2的次方!transient Node<K,V>[] table;// 内部类 EntrySet,值对缓存transient Set<Map.Entry<K,V>> entrySet;// table中Node的数量transient int size;// 结构更改的次数。与AbstractList类似 (See ConcurrentModificationException) transient int modCount;// 下次需扩容size阈值: capacity * loadFactor, 或 外部指定initCap时tableSizeFor方法计算出的初始容量int threshold;// 加载因子 用于确定thresholdfinal float loadFactor;

loadFactor

加载因子表示hash表中元素的填满的程度, 默认是0.75。因子越大,填满的元素越多, 好处是:空间利用率高了, 但冲突的机会加大了;因子越小,则反之。

冲突的机会越大带来查找成本越大,所以需要在二者间寻求平衡。

threshold

构造函数指定initialCapacity时,通过tableSizeFor方法计算出的初始容量值;

其他时候表示下次需扩容时变量size的阈值threshold = capacity * loadFactor

构造函数

只是确定好几个成员变量的初值,并不实例化table。真正的实例化是在resize方法中

  public HashMap(int initialCapacity, float loadFactor) {    if (initialCapacity < 0)      throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);    if (initialCapacity > MAXIMUM_CAPACITY)      initialCapacity = MAXIMUM_CAPACITY;    if (loadFactor <= 0 || Float.isNaN(loadFactor))      throw new IllegalArgumentException("Illegal load factor: " + loadFactor);    this.loadFactor = loadFactor;    this.threshold = tableSizeFor(initialCapacity);  }

这里有个重要的方法 tableSizeFor,保证table的初始容量是2的次方

n |= n >>> 1 :  先计算>>>, 按位或后赋值. 等价于 n = n | (n >>> 1)

  // 返回 2 的 次方  static final int tableSizeFor(int cap) {    int n = cap - 1;    n |= n >>> 1;    n |= n >>> 2;    n |= n >>> 4;    n |= n >>> 8;    n |= n >>> 16;    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  }

外部指定initialCapacity时,该方法返回  >= initialCapacity 最接近的 2的次方.

Node

链表结构下的值对存储对象,保存key在hash方法得到的hash值,并链接下一个Node

static class Node<K,V> implements Map.Entry<K,V> {final int hash;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;}        ...

TreeNode

红黑树 结构下的存储对象,继承自LinkedHashMap.Entry 依然可以维护双向链表的结构。超类依然是 HashMap.Node 

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {    TreeNode<K,V> parent;  // red-black tree links    TreeNode<K,V> left;    TreeNode<K,V> right;    TreeNode<K,V> prev;    // needed to unlink next upon deletion    boolean red;    TreeNode(int hash, K key, V val, Node<K,V> next) {    super(hash, key, val, next);        }        ...     }static class Entry<K, V> extends HashMap.Node<K, V> {  Entry<K, V> before, after;  Entry(int hash, K key, V value, Node<K, V> next) {    super(hash, key, value, next);  }}  

树化

链表操作O(n)随N的增长而性能愈差,jdk8将达到阈值的链转换为树。

putVal

调用入口 put mergecompute 方法。

在链表末尾新增节点后,判定: 链长度 > TREEIFY_THRESHOLD 时,执行treeifyBin方法:

putVal -> treeifyBin 

判定 table.length  < MIN_TREEIFY_CAPACITY :   

true 则 resize()扩容 ;   false 则 转换为树结构 -> treeify方法

resize

实例化table

  1. putVal 方法判定table为空 则 执行resize()来初始实例化 

  2. putVal 方法执行结束前,判定 size > threshold  执行 resize()扩容

resize -> split 

判定table[index]是TreeNode,执行split方法来处理重定位时TreeNode是否需要树转链。

区分好移动与否的节点集合后:一定 由 index 移动到 index+ oldCap

所以直接判定各自节点数量 <= UNTREEIFY_THRESHOLD: 转为链结构 -> untreeify方法

重定位

put操作对于链表是 后插入

在低版本中,resize()重定位操作移动到同一新index下的Node链是 前插入下,原链 A->B->nil 对于错误线程可能演变为循环链 A->B->A。

在JDK8中优化了重定位方法来保证移动后节点在链表中的相对先后顺序不变。
(node.hash & oldCap) == 0 则index不变;否则在新table上移动:newIndex = oldIndex + oldCap。

推演

resize():若需要移动,一定是由 index 到 index + oldCap; 

换而言之:table[index+ oldCap] 上的节点一定是由index移动而来。

前提:

  • table.length 一定是 2 的次方。
    (默认是 1 << 4 ;  指定initCap则经过 tableSizeFor处理,保证是2的次方)

  • table扩容大小翻倍:  newCap = oldCap << 1  左移1位

  • 定位:index =  node.hash &  ( cap -1 )    

oldCap = 16   newCap = oldCap << 1 =  32旧下标位置:  e.hash & (oldCap-1) :eg1:hash   二进制值   e.hash =  10     0000 1010 oldCap-1 =  15     0000 1111      &   =  10     0000 1010  eg2:hash   二进制值   e.hash =  17     0001 0001 oldCap-1 =  15     0000 1111      &   =  1      0000 0001比较判定Node在新table的位置是否需要移动:  e.hash & oldCap eg1:hash   二进制值 e.hash  =  10     0000 1010 oldCap  =  16     0001 0000     &   =  0      0000 0000       为0 eg2:hash   二进制值 e.hash  =  17    0001 0001 oldCap  =  16    0001 0000     &   =  1     0001 0000        不为0 新下标位置: e.hash & (newCap-1)eg1:        hash   二进制值   e.hash = 10 0000 1010 newCap-1 = 31 0001 1111          &   = 10 0000 1010结论:下标不变eg1:        hash   二进制值   e.hash = 17 0001 0001 newCap-1 = 31 0001 1111          &   = 17 0001 0001    oldIndex + oldCap = 1 + 16结论:元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长度

在上例中:

oldCap       =  16  0001 0000newCap       =  32  0010 0000  oldCap左移1位,末尾补0(oldCap - 1) =  15  0000 1111(newCap - 1) =  31  0001 1111  (oldCap - 1) 左移1位,末尾补1

(newCap - 1)  与 (oldCap - 1) 二者差别在最高位:(oldCap - 1)是 0  , (newCap -1) 是 1 。

所以:

 hash & ( oldCap - 1) 与 hash & (newCap -1)  不同之处在最高位余下位的值是相同的,正好对应oldIndex值  ;

加之:

(newCap -1) 与 oldCap 的 相同之处是 最高位都是1 。且  oldCap 除高位外余下位数固定是 0; 

所以 :

(hash & oldCap)运算后只会在oldCap的最高位上结果不同,其余位(即使hash位数大于oldCap位数) "因oldCap除高位外余下位数都是0 " 而为0。

由此推出:newIndex = oldIndex + 最高位&运算结果值 

oldCap 的最高位是 1 ,所以取决于hash值在oldCap最高位上的数值

最高位是 0 则 不变 -> newIndex = oldIndex

最高位是 1 则 移动 -> newIndex = oldIndex + oldCap 。

(非2的次方,  除最高位后余下位不一定是0。所以 (hash & oldCap)运算后,不仅最高位的结果会不同,余下位的结果也可能不同。无法推出结论等式,不成立)

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!