前言
基于的版本为JDK1.8的,不过红黑树的方法详解,不会在这里去说,之后会有数据结构,来对此解释,HashMap的源码,涉及了大量的二进制的操作,不了解的,可以看下我发的二进制的解释。
构造器:
HashMap有四个构造器,这里介绍了三个常用的构造器。
public ExHashMap() {
//加载因子,默认为0.75,用于确定临界值的
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public ExHashMap(int initialCapacity) {
//加载因子,默认为0.75,用于确定临界值的
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public ExHashMap(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:
最后一个构造器里使用到了tableSizeFor方法,这个方法用于初始化threshold的变量,该变量的意义代表临界值的意思,当size的实际的数组大小大于threshold时,进行扩容。
//返回的一定是一个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;
}
可能有朋友看不懂这个方法的作用,该方法的厉害之处在于,只要你输入的是一个大于0的数,返回的就是一个2次幂的数,返回的数为cap的最小2次幂,即cap=15返回16,cap=16返回16,cap=17返回32。
int n=cap-1;
作用:是为了防止当cap已经是2次幂的时候,返回的确是这个cap的2倍,减1则保证了返回的一定是最小的2次幂(可以看完这个方法的整个介绍后,在回来看)
之后进行移位操作,并且进行或的操作,这里给个实例看,方便大家理解。
cap=18 =====>0001 0010
n=17 ======>0001 0001
n>>>1=0000 1000
n |= n>>>1 ======>0001 0001 |= 0000 1000=0001 1001
n>>>2=0000 1100
n |= n >>> 2 =====>0001 1001 | 0000 1100=0001 1101
n >>> 4=0000 1110
n |= n >>> 4 ====>0001 1101 | 0000 1110=0001 1111
.....
可以看到到n |= n >>> 4时,n的二进制就已经是 xxxx 11111的形式了,之后的 n |= n >>> 8 、n |= n >>> 16已经不会对其有任何影响了,之后在返回的时候,还会有一个n+1的操作,经过这个操作时,n就会变成 xxx1 000的形式,此时返回的一定是一个2次幂的数。
其实这个方法的操作,将传入的值(不为2次幂时)从最大位数开始,就通过移位和或的操作,将之后的位数的值,全部变为1的操作,之后对移位后的最终值进行一个加1,则最高位数的上一位变为1,之后所有位数的值都为0.
而当传入的值是2次幂时,因为有减1的操作,即最高位n变为0,其余位数为1,之后的操作,不会对其有任何影响,因为其余位数已经为1了,之后在加1,这时最高位,又位1了其余位则为0,等于之前传入的值。
源码中移位操作,每次移的位都是前面的两倍的原因是,第一次移位后,进行或操作,就产生了,两个连续的1,第二次移位后,将上一步的两个连续的1,移动2位后,进行或操作,就可以产生4个1,即每次操作后都将产生上次2倍的移,在将这些1移动2倍的位数,就可以最快的完成目标。
那么为什么一定要返回2次幂的值呢,好处是:
1.提高内存运算速度
2.增加散列度,降低冲突(Length - 1的值的二进制所有的位均为1,这种情况下,Index的结果等于hashCode的最后几位。只要输入的hashCode本身符合均匀分布,Hash算法的结果就是均匀的。)
3.减少内存碎片(操作系统每次申请内存,都是2的倍数)
虽然这里是将返回的值,赋给threshold。但是,请注意,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。
Put方法:
Put方法也是其中较难理解的一个方法,先上代码
public V put(K key, V value) {
//1.key的hash之后用于计算下标
//2.键 3.值
//4.当键相同时,是否会覆盖值,false则会覆盖当前值
//5.HashMap中没有用到,不用思考
return putVal(hash(key), key, value, false, true);
}
hash方法:
//该方法求出该key值加工后的hash值
static final int hash(Object key) {
int h;
//得到hashCode值后,还要将高低16位进行一次异或,用来减少发生碰撞,
//避免之后数组的长度过短,导致hash高位值的作用不大
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
先取到Key的hashCode,在和移位16位后的数组,进行一个异或操作,返回一个加工后的hash值。
这里之所以进行一个异或操作,并且是对移位16位后的数的原因是,避免导致hashCode的高位值,在取模计算时,没有任何的作用。
实例:
//不对其进行异或操作
h=10010100 00011010 10010010
//n表示数组长度减一后的值
n=00000000 00000000 00001111
h & n=00000000 00000000 00000010
h1=11000110 00101000 11010010
n=00000000 00000000 00001111
h1 & n=00000000 00000000 00000010
//h h1两次值不同,但计算出的下标相同,容易发生hash碰撞,分布的并不均匀
//对其进行异或操作
h=0001 1010 1001 0010
h=h ^ (h>>>16) ========> 10010100 00011010 10010010 ^ 0000000 00000000 10010100=10010100 00011010 00000110
n=00000000 00000000 00001111
h & n=10010100 00011010 00000110 & 00000000 00000000 00001111 =====>00000000 00000000 00000110
h1=11000110 00101000 11010010
h1=h1 ^ (h1>>>16) ========> 11000110 00101000 11010010 ^ 0000000 00000000 11000110=11000100 00101000 00010100
n=00000000 00000000 00001111
h1 & n=11000100 00101000 00010100 & 00000000 00000000 00001111======>00000000 00000000 00000100
//h h1两次值不同,但计算出的下标不同,主要针对数组长度不大,而hash的计算只和低位数有关的情况
putVal()方法:
该方法的大致思路是:
1.首先判断数组是否初始化,如果没有则对其扩容
2.之后对加工后的hash进行取模计算,得出对应的下标
3.如果下标处没有插入值,则直接插入
4.如果下标处已经有值了,说明发生了hash碰撞
5.判断头节点的Key值是否为当前需要插入的值,如果是记录当前头节点信息
6.判断数组中存储的是否为红黑数组结构,如果是得到该类型
7.循环遍历链表中的数据,找到了Key相等的节点,先将其记录下来,没有找到,说
明链表中还未插入,则直接插入,插入时需要判断是否达到链表的临界值,如果
达到需要将其转化为红黑树。
8.循环结束后,判断记录的值是否为空,不为空的话,根据onlyIfAbsent值,判断是
否直接覆盖当前节点的value值。
9.如果数量超过临界值,则扩容。
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
//如果用于存储的数组还未初始化
if ((tab = table) == null || (n = tab.length) == 0) {
//则选择扩容
n = (tab = resize()).length;
}
//计算下标的方法为:键的hash值按照数组的长度取余后,得到下标
if ((p = tab[i = (n - 1) & hash]) == null)
//如果该下标处,还未添加对应的链表,则直接赋值即可
tab[i] = newNode(hash, key, value, null);
else {
//该下标位置处,已经有了同样的hash值的元素,需要构建链表或红黑树
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
//如果链表的头节点的key和插入的一致,则先将其记录下来
e = p;
}
else if (p instanceof TreeNode)
//如果是红黑树的结构
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//需要遍历整个链表,找到与他Key相等的节点
for (int binCount = 0; ; ++binCount) {
//如果下个节点为空,则说明该KEY还未插入链表中
if ((e = p.next) == null) {
//直接将其插入到尾节点中去
p.next = newNode(hash, key, value, null);
//如果链表的长度大于临界值的话,将其转化为红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到了KEY值相等的节点,直接跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
//如果记录的值不为NULL,说明链表中有相等KEY的节点
if (e != null) { // existing mapping for key
//得到旧值
V oldValue = e.value;
//根据onlyIfAbsent参数去判断,是否覆盖旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//HashMap中为空方法
afterNodeAccess(e);
return oldValue;
}
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
resize():
//扩容,新数组的大小为旧数组大小的两倍,左移一位
final Node<K, V>[] resize() {
//得到旧的数组,也就是当前数组
Node<K, V>[] oldTab = table;
//旧的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧的临界值
int oldThr = threshold;
int newCap, newThr = 0;
//如果旧的容量大于0,已经初始化过的
if (oldCap > 0) {
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
} else if (oldThr > 0) {
//临界值已经有了,新的容量等于当前临界值,一定为2的幂数
newCap = oldThr;
} else {
//都为0
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * 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;
//创建扩容后的数组
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
//接下来的操作是将旧数组里的数据,按照同样的计算下标的方法放入新的数组中,
//避免扩容后,get不到对应的值了
if (oldTab != null) {
//循环得到旧数组中的数据
for (int i = 0; i < oldCap; i++) {
Node<K, V> e;
if ((e = oldTab[i]) != null) {
//清除旧数组中的值
oldTab[i] = null;
if (e.next == null) {
//如果当前链表只有一个节点,直接添加
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode) {
// 如果该节点是树
// 调用红黑树 的split 方法,传入当前对象,新数组,当前下标,老数组的容量,目的是将树的数据重新散列到数组中
//((TreeNode)e).split(this, newTab, i, oldCap);
} else {
//说明该链表不止一个节点
//用于记录不用更换的下标的链表的头尾
Node<K, V> loHead = null, loTail = null;
//用于记录更换了的下标的链表的头尾
Node<K, V> hiHead = null, hiTail = null;
//记录下一个节点
Node<K, V> next;
do {
next = e.next;
//如果等于0,则说明不用更换下标
if ((e.hash & oldCap) == 0) {
if (loTail == null)
//头节点只要赋值一次即可
loHead = e;
else
//之后的链表直接添加在尾节点的后面
loTail.next = e;
//更新尾节点
loTail = e;
} else {
//与上面思路一致
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//将尾节点之后的节点置为null
loTail.next = null;
//下标和之前一致,直接等于头节点
newTab[i] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//新的下标等于旧下标加旧的容量
newTab[i + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
该方法中,需要注意的几个点:
1.新扩容的大小为旧容量的两倍,如果没有初始化,则等于默认的长度16,此时临界值为12.
2.if ((e.hash & oldCap) == 0) ,关于这个判断的解释为:如果等于0,则说明不用更换下标,
详细解释为,oldCap一定为2次幂,如果其最高位为n,则n的值肯定为1,如果 e.hash & oldCap==0,则说明e.hash的n位数上的值,肯定也为0,扩容后为旧容量的两倍,即 n位为0,n+1位为1,取模时需要长度减一,n+1位为0,n位开始后的所有数都为1,此时和旧容量不同的是,新容量的n位为1,旧容量的n位为0,如果此时e.hash的n位的值为0时,则两次计算一致。
实例:
oldCap=0001 0000
n=0000 1111
h=0100 1001
h & n=0000 1001
newCap=0010 0000
n1=0001 1111
h=0100 1001
h & n1=0000 1001
remove和get
接下来两个方法都较为简单,也比较相似,就不过多解释了
public V get(Object key){
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//得到对应hash值中对应的key的值
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组是否为空,长度是否为0,并且根据KEY的hash值取到对应的下标值
//得到数组中链表
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果头节点的KEY和传入的KEY值,相等:则直接返回该节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果头节点的下个节点不为NULL,则继续寻找
if ((e = first.next) != null) {
//如果为红黑树结构
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历整个链表寻找对应KEY值的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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;
//如果该数组不为NULL,长度大于0,取出对应的节点,赋给P
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;
//如果当前KEY值匹配上了,则说明头节点是对应要删除的节点,先记录下来
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果该头节点下还有节点,即发生了hash碰撞
else if ((e = p.next) != null) {
//判断是否为红黑树
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//循环整个链表,找到对应的节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//如果存在该KEY值相等的节点,记录下来,跳出循环
node = e;
break;
}
//注意p记录的是下一个节点的父节点,即当找到了对应KEY值相等的节点时,p为node的父节点
p = e;
} while ((e = e.next) != null);
}
}
//根据matchValue是否为true,来判断是否需要根据value值是否相等来删除
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);
//node等于p时,说明头节点就是我们需要删除的节点,直接将该节点从链表中剔除即可
else if (node == p)
tab[index] = node.next;
else
//p为node的父节点,此时将p的下一个节点,定义为node的下一个节点即可
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
总结:
1.关于HashMap的源码中的红黑树部分的方法,这里没有写,但之后一定会补上的,欢迎关注。
2.关于上面的注释、例子和说明,肯定会有一些不对的地方,欢迎朋友们指出,不是特别清楚的地方也欢迎讨论。
3.接下来还要LinkedHashMap和一些集合并发的源码。