HashMap
实现Map接口
1.7 链表+数组,数组–Entry
1.8 链表+数组+红黑树,节点—Node
数组:存储区间连续,占用空间多,查找快
链表:查找效率低,删改快
线程不安全,
hashtable安全,但是效率低,因为里面都添加了synchronized
Concurrenthashmap线程安全,采用分段锁
Collections.SynchronizedMap也是另一种线程安全
Note:
JDK1.7之前会在多线程下发生死循环,是因为JDK1.7解决冲突的链表是使用头插法的
JDK1.8使用尾插法解决了上述问题
HashMap 成员变量(Member)
DEFAULT_INITIAL_CAPACITY:默认初始化容量大小(一般都是16),或者创建的时候可以自定义容量
Tips:最后初始化的容量都会是2的幂次(内部通过一堆位运算)
/**
* 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;
}
MAXIMUM_CAPACITY:最大的容量, 1<<30 (30次方),因为第一位JAVA是要存放符号位的,JAVA中由于桶的数量必须是2的幂次,而int的最大 最大数量是 (2<<31)-1,很明显不符合桶数量的要求,所以最大只能是 2<<30
**DEFAULT_LOAD_FACTOR:**默认的负载因子,一般是0.75f
TREEIFY_THRESHOLD: 一个桶中bin(箱子)的存储方式由链表转换成树的阈值。即当桶中bin的数量超过TREEIFY_THRESHOLD时使用树来代替链表。默认值是8.
UNTREEIFY_THRESHOLD: 执行resize操作时,当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6
MIN_TREEIFY_CAPACITY: 当桶中的bin被树化时最小的hash表容量。(如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作)这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。也就是根据默认来算64
Tips: 哈希表的链表树化成红黑树有 两个条件:
- 链表长度大于TREEIFY_THRESHOLD
- 哈希数组的长度大于MIN_TREEIFY_CAPACITY
HashMap成员方法(Method)
static final int tableSizeFor(int cap): 传进来的 table 值最好是2的N次方,如果传进来不是,会通过这个方法进行多次>>> 最终得到一个大于输入参数且最近的2的整数次幂的数
**final Node<K,V>[] resize()😗*1.8 的resize,把创建table和扩容table都放到了一起,
先判断是否有oldTable,还有拿到old threshold 临界值
新的就初始化,threshold 是 DEFAULT_INITIAL_CAPACITY初始化容量 * DEFAULT_LOAD_FACTOR 加载因子
public V put(K key, V value)
实际调用 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
-
先判断当前table是否为空,如为空则调用resize(),创建一个table,判断边界值(threshold),初始边界值为0,赋值给oldThr(旧边界值),然后计算新的边界值
-
根据计算出来的hash值,与刚创建的table长度,进行 & (按位与) 运算 , if null 则创建一个新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); -
否则与原有元素判断 hash 是否相等,然后判断key 是否相等,都不相同,key不同,则判断是否是红黑树结构,如果相同,new value 覆盖 old value
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
p = tab[i = (n - 1) & hash]
Note:不同的对象计算出来的hash值可能相同可能不同;相等就发生hash冲突,key地址判断,不同则判断内容是否相同
4. 如果发生hash冲突,往链表上查找是否有相同如没有则放上去;最后判断链表长度,查看是否满足条件转换红黑树,break
5. 如不是红黑树结构,将新数据放入到 p.next(尾插法), JDK1.7 是头插法
static final int hash(Object key): (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) 提供数组下标,右移16位为的是更均匀的获得数组下标,提高了高位参与运算的数量,散列会更加随机.
Note:
由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。
所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算。
Question:为什么用^而不用&和|
因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^.
转化红黑树
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;
}
从0遍历节点,binCount计算个数 ,当大于8时
进入this method
final void treeifyBin(Node<K,V>[] tab, int hash):
里面会经过这个判断: if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
数组长度小于MIN_TREEIFY_CAPACITY,会进行扩容,即使链表大于8
Resize():扩容是2*旧容量来进行扩容,扩容后会重新计算hash值
红黑树转变
TreeNode<K,V> hd = null, tl = null; hd是红黑树头节点 ,tl是红黑树尾结点
Note:该方法还未转为红黑树,只是设置好了节点而已
if ((tab[index] = hd) != null)
hd.treeify(tab);
final void treeify(Node<K,V>[] tab):This method开始转变为红黑树
说明: Forms tree of the nodes linked from this node.
HashMap扩容机制
When: HashMap中的元素个数超过大小Capacity(数组长度)*loadFactor(负载因子) 就会进行扩容 (扩容异常消耗性能 –因为每次扩容都要重新进行hash分配);
Rehash方式: 2倍原数组长度,便且重新排列,通过
hash扩容后,因为是2倍原数组长度,
所以就是相当于参与计算索引&运算的时候,
会在高位多出一位进行运算,所以在扩容后重新计算hash值的时候,该数据存放的位置要不就是原位置,要不就是 原位置+原数组长度的位置,取决于重新hash后,首位是0还是1的区别,0原位置,1就一倍旧长度位置