主干结构
1.主干结构 Node<K,V>[] table
Node<K,V> implement Map.Entry<K,V> //Node点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node<K,V> next; //链表的下一个node
Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
2.HashMap哈希桶数组table的长度length大小必须为2的n次方(一定是合数),主要是为了在取模和扩容时做优化,同时减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
3.而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能
初始值创建 Map tableSizeFor()
tableSizeFor(int cap) //通过一个初始容量值 创建map 返回一个等于大于 cap 并且最接近cap 并且是2的次幂的数
//cap 传入的容量参数
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;
//经过上述 n|=n-1等五步,该算法让最高位的1后面的位全变为1。例如 01000 -> 01111.
// n = cap-1 避免 cap =2次幂造成的问题,例如 cap =8 的时候 返回的就是 16,我们想要的其实是8.
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
HashMap 计算 table[] 的索引位置
HashMap 计算 table[index]
// 方法一,jdk1.8 & jdk1.7都有:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 方法二,jdk1.7有,jdk1.8没有这个方法,但是实现原理一样的:
static int indexFor(int h, int length) {
return h & (length-1); //aka h % length
}
三步原则
(1) 取key的hashCode值,h = key.hashCode()
(2) 高位参与运算,h ^ (h >>> 16)
(3) 取模运算,h & (length-1)
当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
HashMap的put(K,V)方法
1.判断 table.length == 0,如果为0 进行 resize()扩容;
2.如果 table不为空数组,如果 key 进行 getIndex(key) 找到所在数组的索引位置,如果 table[index] == null,直接插入;如果key不存在,则判断 tbale[index]是否是treeNode 结构,如果是treeNode,则插入<K,V> 然后进行balanceTreeNode 平衡二叉树;如果不是treeNode,插入键值对 判断链表长度是否 >8 如果 >8,则treeify() 链表转为红黑树,如果插入的时候 key有一样的,则覆盖原来的value,并将原来的value return;最后modCount++,判断新增元素后的table[]的长度是否 > threshold [是否需要扩容]。
HashMap的扩容 resize()
resize() //扩容
1.如果 table[]为null的时候 默认设置长度为16的初始数组,threshold=16 * 0.75;如果table有Node,则设置长度为 16 << 1 == 16 * 2, threshold = 12 << 1 == 12 * 2。
2.oldTable里面的数据复制到新的table中,其中有三种情况,node节点是一个单节点,没有形成链表,直接复制 newTab[e.hash & (newTabLength -1)] = e;
3.node.next有节点,说明node已经形成一个链表结构或者是红黑树结构,当e instanceof TreeNode,是一个树形结构的话,会通过 (e.hash & oldTabLength) 判断 是在老的索引还是新的索引,如果(e.hash & oldTabLength) ==0 则e 在老的索引,如果(e.hash & oldTabLength) ==1 则 e在新的索引,新的索引=[e.hash & (oldLength - 1) + oldLength], TreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null; 分别存放 (e.hash & bit) == 0) 索引不改变的node节点,以及(e.hash & bit) == 1 新索引的node节点,然后再次通过 >UNTREEIFY_THRESHOLD(6)去判断是否需要继续构建成树形结构,如果节点数 >UNTREEIFY_THRESHOLD(6) 则通过treeify 构建成树形结构,如果小于 6, 则通过 untreeify 树形节点转为list链表。
重点: 使用 (e.hash & oldTabLength == 0) 来判断是否需要在新的数组中改变索引位置 e.hash & oldTabLength == 1 需要改变索引位置,新的索引为: oldIndex+oldLength。
都是内存指向的改变
使用loHead loTail 高低位来进行复制Node
小记
DEFAULT_INITIAL_CAPACITY = 1<< 4 //aka 16 HashMap的默认数组大小
threshold //下一次HashMap扩容的临界值
tableSizeFor(int cap) //通过一个初始容量值 创建map 返回一个等于大于 cap 并且最接近cap 并且是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;
//经过上述 n|=n-1等五部,该算法让最高位的1后面的位全变为1。例如 01000 -> 01111.
// n = cap-1 避免 cap =2次幂造成的问题,例如 cap =8 的时候 返回的就是 16,我们想要的其实是8.
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
位运算:
8421 原则
1111
2的幂数 10000 原则
n << 1 == n *2
n >> 1 == n /2
treeify() // 构建红黑树。
如果 node元素是一个节点,node.next==null,没有形成链表或者tree,则newTab[e.hash & (newTabLength -1)] = e;如果 node是一个tree
(1) 扩容是一个特别耗性能的操作,所以使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) HashMap是线程不安全的,在并发的环境中建议使用ConcurrentHashMap。 Collections.SynchronizedMap(Map)返回一个同步的map
(3) JDK1.8引入红黑树大程度优化了HashMap的性能,这主要体现在hash算法不均匀时,即产生的链表非常长,这时把链表转为红黑树可以将复杂度从O(n)降到O(logn)。
(4)HashMap是如何工作的?面试时可以这么回答:
HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值
来源:oschina
链接:https://my.oschina.net/u/2870118/blog/3158149