HashMap-resize重定位

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 

常量

 // 默认初始化容量 16
static 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;
// 加载因子 用于肯定threshold
final float loadFactor;

loadFactorjava

加载因子表示hash表中元素的填满的程度, 默认是0.75因子越大,填满的元素越多, 好处是:空间利用率高了, 但冲突的机会加大了;因子越小,则反之。
冲突的机会越大带来查找成本越大,因此须要在两者间寻求平衡。
node

threshold数组

构造函数指定initialCapacity时,经过tableSizeFor方法计算出的初始容量值(2的次方);其余时候表示下次需扩容时变量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的次方性能

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

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

  // 返回 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; }

Node

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

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;} ...

hashMap无序 & LinkedHashMap 有序

  1. LinkedHashMap.Entry 超类是 HashMap.Node ,但增长先后指向,维护双向链表的结构。
  2. LinkedHashMap 有成员变量(head、tail)来指向链表的首尾端。

HashMap的访问(values()、keySet()、entrySet())遍历是table[] 数组; 而LinkedHashMap的访问遍历是其维护的双向链表。线程

eg.

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(8) 时,执行treeifyBin方法:

putVal -> treeifyBin 

断定 table.length  < MIN_TREEIFY_CAPACITY (64) :   
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 (6):  转为链结构 -> untreeify方法

重定位

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

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

推演resize()

  1. 若须要移动,必定是由 index 到 index + oldCap; 换而言之:table[index+ oldCap] 上的节点必定是由index移动而来。
  2.  小于等于(oldCap-1)的数不会移动。 由于在oldCap最高位(含)向左 都是 0 。

前提:

  • 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)运算后,不只最高位的结果会不一样,余下位的结果也可能不一样。没法推出结论等式,不成立)

Q&A

HashMap:  无限制
HashTable:  Key与Value都不能为null
TreeMap:  Key不能为null (经过Key来排序,因此Key要继承java.util.Comparator
ConcurrentHashMap: Key与Value都不能为null

Q: 为何链表与红黑树互转的阈值是六、8 ?
A:若是选择6和8(若是链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7能够有效防止链表和树频繁转换。假设一下,若是设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,若是一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

Q: 为何加载因子loadFactor默认是 0.75 ?
A:理想状况下,在随机哈希码下容器中的节点遵循泊松分布

Q: HashMap 中的 key若 Object类型, 则需实现哪些方法 ?
A:

这里也间接代表了: hashCode() 相同,equals()  不必定相同(所谓的Hash碰撞); 而 equals() 相同, hashCode() 必定相同。

Q: 为何 HashMap 中 String、Integer 这样的包装类适合做为 key 键 ?
A:
相关文章
相关标签/搜索