思维导图
思维导图是个好东西,哈哈
继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
可以看出具有map的通性,也能够被克隆
底层结构
hashMap的hash算法
JDK1.7中
static int hash(int h) {
// 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);
}
扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算
JDk1.8中
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);////key.hashCode()为哈希算法,返回初始哈希值
}
扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
key.hashCode()是Key自带的hashCode()方法,返回一个int类型的散列值。
32位带符号的int表值范围从-2147483648到2147483648,大概40亿的样子。
这么大的值里,一般是很难发生碰撞的,但是内存放不下这么长的数组,同时HashMap的初始容量只有16,
所以这样的散列值使用需要对数组的长度取模运算,得到余数才是索引值。
int index = (n - 1) & hash;
为什么HashMap的数组长度是2的整数幂?
以初始长度为16为举例,16-1 = 15,15的二进制数位是0000 0000 0000 0000 0000 1111,
再比如31的二进制是0000 0000 0000 0000 0001 1111,
可以看出一个基数二进制最后一位必然位1,当与一个hash值进行与运算时,最后一位可能是0也可能是1。
但偶数与一个hash值进行与运算最后一位必然为0,造成有些位置永远映射不上值。
相与下来高位全部为0,只保留低位,
但是这时,又出现了一个问题,即使散列函数很松散,但只取最后几位碰撞也会很严重。
这时候hash算法的价值就体现出来了,
扰动函数
hashCode右移16位,正好是32bit的一半,
与自己本身做异或操作(相同为0,不同为1),
也是为了混合哈希值的高位和低位,增加低位的随机性,
同时混合后的值也变相保持了高位的特征。
JDK1.7的底层结构
数组+链表
JDK1.8的底层结构
数组+链表+红黑树
先是数组加链表结构,当同一个hash值存放超过8个元素时,即当链表长度超过阈值(8)时,转换为红黑树结构,加快查询速度。
初始化
一些基本属性table、entrySet、size、modCount、threshold、loadFactor
/**
* 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.)
* 表格,在第一次使用时初始化,调整大小为必要的。当分配时,长度总是2的幂。(在某些操作中,我们也允许长度为零目前不需要的引导机制。)
* 其定义为 Node<K,V>[],即用来存储 key-value 的节点对象。在 HashMap 中它有个专业的叫法 buckets ,中文叫作桶。
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
* 保存缓存entrySet ()。注意,这里使用了AbstractMap字段表示keySet()和values()。
*
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
* 此映射中包含的键-值映射的数目
* 容器中实际存放的node大小
*/
transient int size;
/**
* 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).
* 这个HashMap在结构上被修改的次数,结构修改是指改变映射的数量HashMap或修改其内部结构(例如,重复)。
* 记录容器被修改的次数
*/
transient int modCount;
/**
* 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;
/**
* The load factor for the hash table.
* 负载因子 默认是0.75,综合考虑了时间和空间利用率
* @serial
*/
final float loadFactor;
内部桶的源码
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
* 基本哈希bin节点,用于大多数条目。(见下文TreeNode子类,在LinkedHashMap中为它的Entry子类。)
*/
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;
}
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);//重写hashcode方法
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//重写equals方法
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;
}
}
无参构造方法
这是用得最多的构造方法
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
* 使用默认的初始容量构造一个空的HashMap,默认大小16,默认负载因子为0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
指定大小的构造方法
默认负载因子是0.75
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative. 当初始化大小为负数时,抛出异常
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
指定大小和负载因子的构造方法
这种情况下需要算出阈值。
如果操出了map的最大值,即2的30次方,就将值设置为最大值,
如果负载因子为负或者不是数字类型,都抛出异常。
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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方法获取大于该初始值的2的n次方值。
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
前面所有的过程,都是保证造成一个所有位都位1的数据。并且通过最后的+1。变成2N次方格式的数据。
小结
一般在确定元素个数的情况下,还是使用传入初始值的构造方法。
常见api
put
源码解读
1.7的put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //如果哈希表没有初始化(table为空)
inflateTable(threshold); //用构造时的阈值(其实就是初始容量)扩展table
}
//如果key==null,就将value加到table[0]的位置
//该位置永远只有一个value,新传进来的value会覆盖旧的value
if (key == null)
return putForNullKey(value);
int hash = hash(key); //根据键值计算hash值
int i = indexFor(hash, table.length); //搜索指定hash在table中的索引
//循环遍历Entry数组,若该key对应的键值对已经存在,则用新的value取代旧的value
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))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //并返回旧的value
}
}
modCount++;
//如果在table[i]中没找到对应的key,那么就直接在该位置的链表中添加此Entry
addEntry(hash, key, value, i);
return null;
}
addEntry 源码,涉及扩容
void addEntry(int hash, K key, V value, int bucketIndex) {
// 键值对数量超过阈值 并且 当前元素要存放的位置不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); // 扩容(注意这里的扩容值和1.8的不一样)
hash = (null != key) ? hash(key) : 0; // 重新计算哈希值
bucketIndex = indexFor(hash, table.length); // 重新计算下标
}
// 加入新结点
createEntry(hash, key, value, bucketIndex);
}
流程图:
1.8的put方法
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
* 使 key 和 value 产生关联,但如果有相同的 key 则新的会替换掉旧的
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, dont change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
// evict: true <=> 插入结点后是否允许操作(给子类LinkedHashMap用的)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//当table为空的时候会进行扩容,插入第一个值的时候发生
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算下标,看是否有冲突,即是否存在值
if ((p = tab[i = (n - 1) & hash]) == null)
//没有冲突直接插入
tab[i] = newNode(hash, key, value, null);
//有冲突
else {
Node<K,V> e; K k;
// 如果头结点是要找的结点(key的哈希值并且key的值相同),e指向p
if (p.hash == hash &&//hash值相同
((k = p.key) == key || (key != null && key.equals(k))))//key值相同
e = p;
//如果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);//尾插法插入
// 判断是否超出树的阈值(8),是则树化,但是也要看是否超过了数组是否超过64,否则也只是执行扩容操作,具体看treeifyBin方法
// -1是因为除去了数组内的头结点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//进行树化
treeifyBin(tab, hash);
break;
}
// 对链表中的相同 hash 值且 key 相同的进一步作检查
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 找到目标结点,覆写(如果允许的话 或 旧值为null)并返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;// 操作次数++
//判断扩容条件与1.7相比判断条件少了null != table[bucketIndex]
if (++size > threshold)
//扩容
resize();
//用处不大(与LinkedHashMap有关)
afterNodeInsertion(evict);
return null;
}
treeifyBin 方法源码
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* 替换为给定散列的bin at索引中的所有链接节点,除非表格太小,这种情况下会调整大小。
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//判断table长度是否小于64,如果是,先进行扩容。
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);
}
}
MIN_TREEIFY_CAPACITY 参数,可被树化的最小表容量
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
* 容器可能被treeified的最小表容量。(否则,如果容器中有太多节点,将调整表的大小。)
* 至少为4 * TREEIFY_THRESHOLD,以避免冲突调整大小和treeification阈值之间。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
流程图:
put方法的变化
主要是1.8减少了resize的判定条件,增加了当同一个桶元素到达8个时,链表转化为树,8由头插法变成的尾插法,避免了循环。
get
源码解读
jdk1.7的get
public V get(Object key) {
// key为空,调用getForNullKey方法,因为空键固定放在0号位
if (key == null)
return getForNullKey();
// 得到key所在的结点
Entry<K,V> entry = getEntry(key);
// 结点为空直接返回null,反之获取对应val
return null == entry ? null : entry.getValue();
}
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
getEntry 方法
final Entry<K,V> getEntry(Object key) {
// 空map返回null
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key); // 得到key的哈希值
// 计算key对应下标i,并遍历i号位的链表,找到key值对应的结点
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 找到键为key的结点,判断条件:
if (e.hash == hash && // 1.key的哈希值相同
((k = e.key) == key || (key != null && key.equals(k)))) // 2.key的值相同
return e;
}
// 找不到就返回null
return null;
}
jdk1.8的get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode方法,获取元素的关键
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 // 数组长度大于0
&& (first = tab[(n - 1) & hash]) != null) { // 计算下标i,i号位不为空
if (first.hash == hash && // always check first node 首先检查头结点是否是目标结点,是就返回
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果头结点后连得第一个是树节点,调用树的get方法
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);
}
}
// 找不到就返回null
return null;
}
get方法的变化
8相对7增加了一个树的查询操作
remove方法
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
removeNode方法
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//当table不为空,并且hash对应的桶不为空时
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//桶中的头节点就是我们要删除的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//用node记录要删除的头节点
node = p;
//头节点不是要删除的节点,并且头节点之后还有节点
else if ((e = p.next) != null) {
//头节点为树节点,则进入树查找要删除的节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//头节点为链表节点
else {
//遍历链表
do {
//hash值相等,并且key地址相等或者equals
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//node记录要删除的节点
node = e;
break;
}
//p保存当前遍历到的节点
p = e;
} while ((e = e.next) != null);
}
}
//我们要找的节点不为空
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
//在树中删除节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//我们要删除的是头节点
else if (node == p)
tab[index] = node.next;
//不是头节点,将当前节点指向删除节点的下一个节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
removeTreeNode 方法
/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
* 删除在此调用之前必须存在的给定节点。这比典型的红黑删除代码更混乱,因为我们不能与叶子交换内部节点的内容
* 被可访问的“next”指针固定的后继者遍历期间独立完成。所以我们换了树联系。如果当前树看起来节点太少,
* 箱子被转换回普通的箱子。(测试触发在2到6个节点之间,取决于树的结构)。
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);//平衡二叉树方法
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}****
扩容
jdk1.7的resize
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
* 把当前map中的元素翻新到新的更大的新map中。当前map中的键值对达到阈值就会触发扩容方法。
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
* 如果当前容量已经达到最大容量,map将不会进行扩容,而是将阈值提到Integer.MAX_VALUE,从而达到以后不会再调用扩容方法的效果
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
* 新的容量,必须是2的幂次,必须比当前容量大,除非当前容量已经达到最大容量
*/
void resize(int newCapacity) {
Entry[] oldTable = table; // 当前数组
int oldCapacity = oldTable.length; // 当前容量
// 已经达到最大容量的情况下,将阈值升到Integer.MAX_VALUE,能够有效阻止日后调用扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity]; // 创建新数组
transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 将旧数组中的元素全部转移到新数组
table = newTable; // 新数组
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); // 重新计算阈值
}
transfer方法
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历原数组的元素
for (Entry<K,V> e : table) {
while(null != e) {
// 遍历链表
Entry<K,V> next = e.next; // 指针记录e.next
// 如果需要,重新计算哈希值
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); // 重新计算下标
// 头插法
e.next = newTable[i]; // 将e插入到table[i]头部之前
newTable[i] = e; // 将链表下移
e = next; // 指向下一个要移动的结点
}
}
}
jdk1.8的resize
涉及两个关键参数,即两个关键阈值
链表转为数结构阈值
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 对象使用树而不是列表时的bin计数阈值本。当向a添加一个元素时,箱子被转换为树至少有这么多节点的bin。
* 这个值必须更大大于2,且至少为8,以符合假设删除关于转换回普通箱子的树收缩。
*/
static final int TREEIFY_THRESHOLD = 8;
树转为链表结构阈值
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
* 存储计数阈值,用于在存储期间取消(分割)存储调整操作。应该小于TREEIFY_THRESHOLD,在大多数6到网格与收缩检测下去除。
*/
static final int UNTREEIFY_THRESHOLD = 6;
resize方法
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* 初始化或加倍表的大小。如果为空,则在中分配符合现场阈值的初始容量目标。
* 否则,因为我们使用的是2的乘方展开
* 每个容器中的元素必须保持在相同的索引中,或者移动新表中偏移量为2的幂次。
* 初始化或翻倍数组。如果为空,初始化数组。
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 暂存旧table数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 暂存旧容量,如果是初始化调用扩容方法,旧容量为空,赋0
int oldThr = threshold; // 暂存旧阈值
int newCap, newThr = 0;
// 旧容量不为0
if (oldCap > 0) {
// 旧容量达到最大容量,不再扩容,将阈值提高至Integer.MAX_VALUE,有效阻止以后再度出现达到阈值的情况
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 //两倍旧阈值
}
// 如果旧容量为0,即table数组还未创建
// 为带参构造方法使用HashMap(int initialCapacity)及HashMap(int initialCapacity, float loadFactor)
// 因为初始化容量一开始是被存放在threshold中的
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 旧容量为0,即table数组还未创建
// 为无参构造方法使用HashMap(),使用默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 初始化阈值
}
// 如果新阈值为0
// 一种情况是使用了带参构造方法(else if (oldThr > 0))
// 另一种是旧容量未达到默认容量大小或翻倍后超出最大容量(else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
// oldCap >= DEFAULT_INITIAL_CAPACITY))
if (newThr == 0) {
float ft = (float)newCap * loadFactor; // 计算新阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); // 新容量满足要求
}
// 赋阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 赋给全局变量
// 这里开始实施转移,transfer
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) // 树节点,调用树的split方法
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 链表结点,保留之前的顺序(尾插法)
else { // preserve order
// 第二层,链表遍历
// (我们以该链表原本位置在010(2),旧容量为100(4)举例子,分析已知原本的哈希值可能为010,可能为110
// head记录头结点,tail记录尾结点
Node<K,V> loHead = null, loTail = null; // lo表示low表示0,记录要转移的结点
Node<K,V> hiHead = null, hiTail = null; // hi表示high表示1,记录保留在原位置的结点
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 高位为0(010
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 高位为1(110
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 高位为0的链表,保持原位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位为1的链表,转移阵地
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新数组
return newTab;
}
小结JDK1.7和JDK1.8的resize方法的不同点
1.扩容条件不同 7是,达到阈值并且新增结点发生碰撞才扩容,也就是说,如果达到阈值后,新增节点插入位置为空,则先不扩容。
8是只要达到阈值就扩容,条件减少了。
2.扩容方式不同(针对链表而言)
7是采取和插入时一致的方式,头插法,再下移。
8是拆分链表后直接移动头结点。
3.扩容下标计算方式不同
7是重新计算。
8是根据高位二进制决定,为0则下标不变,为1则往后移动一个旧数组长度的距离。
一些常见问题
什么时候由链表转化为红黑树?
在上面普通方法源码中已经指出来,当同一个entry的数量达到8的时候会调用树化的方法treeifyBin,
但是在treeifyBin方法中,有个限制条件是table数组的长度需要达到64(MIN_TREEIFY_CAPACITY),最小树化容量,
否则就只是扩容,而不会树化。
需要达到最小树化容量的原因,为什么不达到8就直接树化?
1.首先链表的查询时间复杂度是O(n),树的查询时间复杂度是O(logn);
2.由于树形节点的大小大约是常规节点的两倍,所以我们只有当箱子包含足够的节点时才使用它们;
3.属性上规定是4*TREEIFY_THRESHOLD,而默认初始化,大小是16。
4.hashCode算法下所有bin中节点的分布频率会遵循泊松分布,而相同节点下的链表长度达到8的概率为0.00000006,
相当小,从而转换为红黑树的概率也小。
5.在链表数量少的情况下
即当链表长度为6时 查询的平均长度为 n/2=3
红黑树为 log(6)=2.6
为8时 : 链表 8/2=4
红黑树 log(8)=3
可见数量少的情况下,链表的查询速度更快,当超过8,就会是树的查询树洞更快了。
所以table稍等时候可以通过扩容,减少链表的长度,没必要转换为又占空间,又耗时的红黑树。
什么时候由红黑树转化为链表?
分为两种情况
1.在删除元素时
在删除元素时调用removeTreeNode方法中有段源码
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
此处并没有利用到网上所说的,当节点数小于UNTREEIFY_THRESHOLD时才转换,而是通过红黑树根节点及其子节点是否为空来判断
是否需要转换为链表。
在扩容时resize方法中
对红黑树进行了拆分
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
红黑树拆分方法中
树的数量,如果数量小于6,就进行树转为链
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//分为两棵树,分别判断树的数量,如果数量小于6,就进行树转为链
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
untreeify 解除树方法
/**
* Returns a list of non-TreeNodes replacing those linked from this node.
*返回一个非树节点列表,替换链接的树节点此节点.
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
为什么是6?
由于为6的时候,链表的查询树洞已经优于树了,同事链表也会更节省空间。
在操作put时也会少去树插入数据的过程。
为什么初始化容器大小是16?
1.首要原因是hash下标的算法,规定容量最好是2的n次方。
static int indexFor(int h, int length) {
return h & (length-1);
}
因为这时候,h&(length-1)(与)才等价于h%length(模)。
2.length的值为偶数,length - 1 为奇数,则二进制位的最后以为为1,
这样保证了h & (length - 1)的二进制数最后一位可能为1,也可能为0。
如果length为奇数,那么就会浪费一半的空间。
3.至于为什么是16,应该是一个经验值,过大过小都不太好。
太小了就有可能频繁发生扩容,影响效率,太大了又浪费空间。
hashmap的负载因子是多大?为什么是这个值?
默认负载因子是0.75,
综合了时间利用率和空间利用率考虑后,选取的这个值。
如果过大比如为1,会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率
如果过小比如为0.5,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,
那么底层的链表长度或者是红黑树的高度就会降低,查询效率就会增加;
但是空间就会增加两倍,牺牲了空间来换取时间。
hashMap使用了哪些方法避免hash冲突?
1.使用链地址法(使用散列表)来链接拥有相同hash值的数据;
2.使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
3.引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
为什么HashMap中String、Integer这样的包装类适合作为K?
1.String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率;
2.都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况;
3.内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况。
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,
而HashMap的容量范围是在16(初始化默认值)~2^30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,
从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
那怎么解决呢?
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度-1)来获取数组下标的方式进行存储,
这样一来是比取余操作更加有效率,
二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,
三来解决了“哈希值与数组大小范围不匹配”的问题。
HashMap 的长度为什么是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。
这个实现就是把数据存到哪个链表/红黑树中的算法。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现,
但是,重点来了:取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作,
也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方,
并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
1.8源码中进行了两次扰动处理,1次位运算 + 1次异或运算。
那为什么是两次扰动呢?
这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,
最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;
写在后面的话
孤独是常态,你需要在学会在孤独中守住内心的美好。
参考
hashmap解析与1.7和1.8 put方法流程图与常见问题
基于jdk1.7和1.8的HashMap源码分析(侧重于哈希算法、put、get、resize)