Android知识总结
一、前言
- 随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。
HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 - HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
- HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
- HasMap 的容量为 2 的幂数,就是为了数据的均匀分布,减少 hash 冲突,毕竟hash冲突越大代表数组中的一个链就越大,这样的话就会降低HashMap 的性能。
二、HashMap基础
HashMap继承了AbstractMap类,实现了Map,Cloneable,Serializable接口
//HashMap的容量,默认是16,扩容是为2的幂即2倍进行扩容
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//阈值,阈值=容量*加载因子。每次扩容是的最大值,超过这个值就扩容。默认 12
int threshold;
//HashMap的加载因子,默认是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap的容量是有上限的,必须小于1<<30,即1073741824。
//如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE,即永远不会超出阈值了)。
static final int MAXIMUM_CAPACITY = 1 << 30;
//由线性链表转化为树的阈值,默认值为 8。桶中bin的数量超过该阈值,就由链表转化为树。
static final int TREEIFY_THRESHOLD = 8;
//由树转化为链表的阈值,默认值为 6。当桶中bin的数量小于该阈值,就将树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//桶中bin 被树化时,最小的hash表容量,默认为 64 。
static final int MIN_TREEIFY_CAPACITY = 64;
//定义一个类型为Node的table数组
transient Node[] table;
//table数组的长度
transient int size
//实际的负载因子, 在构造器中进行初始化
//如果创建HashMap时没有指定loadFactor的大小则会初始化为DEFAULT_INITIAL_CAPACITY的值
final float loadFactor;
//HashMap更改的次数
//用来作为并发下判断是否有其它线程修改了该HashMap,抛出ConcurrentModificationException
transient int modCount;
HashMap 的 数组 Node[] table 初始化的长度 capacity 是16,哈希桶数组长度 length ⼤⼩必须是2的n次⽅,这种设计主要是为了在取模和扩容时做优化(提⾼计算 index索引的效率),同时为了减少冲突,HashMap 定位哈希桶索引位置时,也加⼊了⾼位参与运算的过程。
loadFactor
为负载因⼦,默认值是 0.75,0.75 是对时间和空间效率的⼀个平衡选择,建议⼤家不要修改,除⾮在时间或者空间上⽐较特殊的情况下。例如:如果内存空间很多⽽又对时间效率要求很⾼,可以降低负载因⼦ loadFactor 的值,相反,如果内存空间较少⽽又对时间效率要求不⾼,可以增加负载因⼦ loadFactor 的值,这个值可以⼤于1。
threshold
是 HashMap 所能容纳的最⼤的键值对的个数,threshold = capacity *loadFactor,也就是说 capacity 数组⼀定的情况下,负载因⼦越⼤,所能容纳的键值对个数越多,超出 threshold 这个数⽬就重新 resize(扩容),扩容后的 HashMap的容量是之前的2倍。size 是 HashMap 中实际存在的键值对的数量,注意和 Node[] table 的长度 capacity 、容纳最⼤键值对数量 threshold 的区别。
modCount
主要⽤来记录 HashMap 内部结构发⽣变化的次数,主要⽤于迭代的快速失败。强调⼀点,内部结构发⽣变化是指结构发⽣变化,例如 put 新的键值对,但是某个 key 对应的 value 值被覆盖不属于结构变化TREEIFY_THRESHOLD 和 MIN_TREEIFY_THRESHOLD,即使 hash 算法 和负载因⼦设计的再完美,也避免不了拉链过长的情况,⼀旦出现拉链过长,严重影响 HashMap 的性能,于是在 JDK1.8 中对数据结构做了进⼀步的优化,引⼊了红⿊树。当链表长度太长(超过 TREEIFY_THRESHOLD = 8)时,当Node[] table 数组长度超过 64(MIN_TREEIFY_THRESHOLD = 64) 时,链表就转化为了红⿊树,利⽤红⿊树快速增删改查的特点提⾼ HashMap 的性能。
- 1.3、Node和Node链
HashMap类中的元素是Node类,翻译过来就是节点,是定义在HashMap中的一个内部类,实现了Map.Entry接口。
static class Node implements Map.Entry {
//key的哈希值
final int hash;
//节点的key,类型和定义HashMap时的key相同
final K key;
//节点的value,类型和定义HashMap时的value相同
V value;
//该节点的下一节点
Node next;
Node(int hash, K key, V value, Node 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);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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;
}
}
值得注意的是其中的next属性,记录的是下一个节点本身,也是一个Node节点,这个Node节点也有next属性,记录了下一个节点。而对于一个HashMap来说,只要明确记录每个链表的第一个节点,就能顺序遍历链表上的所有节点。
- 1.4、TreeNode红黑树
当HashMap把链表转为红黑树的时候,原来的Node节点就会被转为TreeNode节点,TreeNode也是HashMap中定义的内部类,定义大概是这样的:
static final class TreeNode extends LinkedHashMap.LinkedHashMapEntry {
//父节点
TreeNode parent; // red-black tree links
TreeNode left; //存放小于父节点的
TreeNode right;//存放大于父节点的
//上一节点
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
//从节点移出元素
static void moveRootToFront(Node[] tab, TreeNode root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode first = (TreeNode)tab[index];
if (root != first) {
Node rn;
tab[index] = root;
TreeNode rp = root.prev;
if ((rn = root.next) != null)
((TreeNode)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
//查找元素
final TreeNode find(int h, Object k, Class> kc) {
TreeNode p = this;
do {
int ph, dir; K pk;
TreeNode pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
...
}
可以看到,TreeNode继承了LinkedHashMap的Entry,TreeNode节点在构造时,也指定了hash值,key,value,下一节点next,这些都是和Node相同的结构。
同时,TreeNode还保存了父节点parent, 左孩子left,右孩子right,上一节点prev,另外还有红黑树用到的red属性。
红黑树是一种近似平衡的二叉查找树,他并非绝对平衡,但是可以保证任何一个节点的左右子树的高度差不会超过二者中较低的那个的一倍。
红黑树有这样的特点:
- 1、每个节点要么是红色,要么是黑色。
- 2、根节点必须是黑色。叶子节点必须是黑色NULL节点。
- 3、红色节点不能连续。
- 4、对于每个节点,从该点至叶子节点的任何路径,都含有相同个数的黑色节点。
- 5、能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。
三、put 元素
- 1、hash值和table.length取模
取模的方法是(table.length - 1) & hash,算法直接舍弃了二进制hash值在table.length以上的位,因为那些位都代表table.length的2的n次方倍数。(table.length - 1) & hash 位运算主要提高运算效率。
取模的结果就是Node将要放入table的下标。
比如,一个Node的hash值是5,table长度是4,那么取余的结果是1,也就是说,这个Node将被放入table[1]所代表的链表(table[1]本身指向的是链表的第一个节点)。
- 2、添加元素
如果此时table的对应位置没有任何元素,也就是table[i]=null,那么就直接把Node放入table[i]的位置,并且这个Node的next==null。
如果此时table对应位置是一个Node,说明对应的位置已经保存了一个Node链表,则需要遍历链表,如果发现相同hash值则替换Node节点,如果没有相同hash值,则把新的Node插入链表的末端,作为之前末端Node的next,同时新Node的next==null。
如果此时table对应位置是一个TreeNode,说明链表被转换成了红黑树,则根据hash值向红黑树中添加或替换TreeNode。(JDK1.8)
3、如果添加元素之后,Node链表的节点数超过了8个,则该链表会考虑转为红黑树。(JDK1.8)
5、如果添加元素之后,HashMap总节点数超过了阈值,则HashMap会进行扩容。
- 添加Value
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 计算hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 判断是否扩容或者转换成红黑树
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//第一次put进入,创建数组并扩容数组个数为 8
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//table对应位置无节点,则创建新的Node节点放入对应位置。
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//table对应位置有节点,如果hash值匹配,则替换
e = p;
else if (p instanceof TreeNode)
//table对应位置有节点,如果table对应位置已经是一个TreeNode,
//不再是Node,也就说,table对应位置是TreeNode,
//表示已经从链表转换成了红黑树,则执行插入红黑树节点的逻辑。
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//创建Node节点,加到上个节点的next位置
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//table对应位置有节点,且节点是Node(链表状态,不是红黑树),所以加上根节点为 8
//链表中节点数量大于TREEIFY_THRESHOLD = 8,则考虑变为红黑树。
//实际上不一定真的立刻就变,table短的时候扩容一下也能解决问题,后面的代码会提到
treeifyBin(tab, hash);
break;
}
//找到相等的key节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//key相等覆盖逻辑
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//更改次数加1
++modCount;
//HashMap中节点个数大于threshold,会进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
执行TreeNode#putTreeVal
添加红黑树节点
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {
// map 当前Hashmap对象
//tab Hashmap对象中的table数组
//h hash值
// K key
// V value
Class> kc = null;
boolean searched = false; //标识是否被收索过
TreeNode root = (parent != null) ? root() : this; // 找到root根节点
for (TreeNode p = root;;) { //从根节点开始遍历循环
int dir, ph; K pk;
// 根据hash值 判断方向
if ((ph = p.hash) > h)
// 大于放左侧
dir = -1;
else if (ph < h)
// 小于放右侧
dir = 1;
// 如果key 相等 直接返回该节点的引用 外边的方法会对其value进行设置
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/**
*下面的步骤主要就是当发生冲突 也就是hash相等的时候
* 检验是否有hash相同 并且equals相同的节点。
* 也就是检验该键是否存在与该树上
*/
//说明进入下面这个判断的条件是 hash相同 但是equal不同
// 没有实现Comparable接口或者 实现该接口 并且 k与pk Comparable比较结果相同
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//在左右子树递归的寻找 是否有key的hash相同 并且equals相同的节点
if (!searched) {
TreeNode q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
//找到了 就直接返回
return q;
}
//说明红黑树中没有与之equals相等的 那就必须进行插入操作
//打破平衡的方法的 分出大小 结果 只有-1 1
dir = tieBreakOrder(k, pk);
}
//下列操作进行插入节点
//xp 保存当前节点
TreeNode xp = p;
//找到要插入节点的位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node xpn = xp.next;
//创建出一个新的节点
TreeNode x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
//小于父亲节点 新节点放左孩子位置
xp.left = x;
else
//大于父亲节点 放右孩子位置
xp.right = x;
//维护双链表关系
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode)xpn).prev = x;
//将root移到table数组的i 位置的第一个节点
//插入操作过红黑树之后 重新调整平衡。
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
添加树形节点,与添加双链表节点个过程类似:
- 1、从root节点开始寻找 如果目标k.hash 小于 当前节点的 hash ,那么到左树寻找,大于那么从右树寻找。如果找到key相同并且equals相同的节点 p 那就直接返回。
- 2、如果hash相同 但是equal不同 进而通过Comparable接口,进行比较,如果比较的结果如果还是相等,则在左右子树递归的寻找是否有与要插入的key equals相同的元素。如果有那么直接return返回。
(也即是没实现Comparable接口,大小由hash判定。实现了,则由Comparable接口的比较方法判定)- 3、如果遍历完所有的节点 并未找到equals相同的节点。那就需要插入该新节点。必须分出大小,所以通过执行tieBreakOrder方法,该方法的返回值是-1,1。如果是-1则插入到左边节点,1就插入到右边节点。
- 4、插入完成之后,需要重新移动root节点 到table数组的i位置的第一个节点上 并且需重新平衡红黑树。
四、转换红黑树
//大于8 转调用换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶中bin 被树化时,最小的hash表容量,默认为 64 。
//当散列表容量小于该阈值,即使桶中bin的数量超过了 treeify_threshold ,也不会进行树化,只会进行扩容操作。
static final int MIN_TREEIFY_CAPACITY = 64;
在HashMap里面定义了一个常量TREEIFY_THRESHOLD
,默认为8。当链表中的节点数量大于TREEIFY_THRESHOLD
时,链表将会考虑改为红黑树,代码是在上面putVal()方法的这一部分。
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//如果小于64进行扩容,不转化 红黑树
resize();
//(n - 1) & hash :是取模运算
else if ((e = tab[index = (n - 1) & hash]) != null) {
//下面是转换红黑树的过程
TreeNode hd = null, tl = null;
do {
//转成红黑树
TreeNode 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);
}
}
TreeNode replacementTreeNode(Node p, Node next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
可以看到,如果table长度小于常量MIN_TREEIFY_CAPACITY
时,不会变为红黑树,而是调用resize()方法进行扩容。MIN_TREEIFY_CAPACITY
的默认值是64。显然HashMap认为,虽然链表长度超过了8,但是table长度太短,只需要扩容然后重新散列一下就可以。
从代码中可以看到,变为红黑树的必要条件是链表的节点是大于8,且table(数组)长度已经大于64。下次执行 resize 判断树的节点个数低于 6,也再会把树转化为链表。
final void treeify(Node[] tab) {
TreeNode root = null;
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class> kc = null;
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
五、HashMap扩容机制
当HashMap决定扩容时,会调用HashMap类中的resize
方法,参数是新的table长度。在JDK1.7
和JDK1.8
的扩容机制有很大不同。
5.1、JDK1.7的扩容机制
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
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);
}
代码中可以看到,如果原有table长度已经达到了上限,就不再扩容了。
如果还未达到上限,则创建一个新的table,并调用transfer方法进行扩容,并重新计算hash值,比较消耗时间
:
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
Entry e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //标记[1]
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
}
transfer方法的作用是把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点
。
static int indexFor(int h, int length) {
return h & (length-1);
}
5.2、JDK1.8的扩容机制
JDK1.8对resize()方法进行很大的调整,JDK1.8的resize()方法如下:
final Node[] resize() {
//得到当前数组
Node[] oldTab = table;
//如果当前数组== null 返回 0,否则返回数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//当前阈值点,默认 12 (16 * 0.75)
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果capacity已经扩容到最大(2^31-1),则不进行扩容
threshold = Integer.MAX_VALUE;
return oldTab;
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
// capacity > 16 且 capacity*2 < MAXIMUM_CAPACITY, 则进行扩容2倍
newThr = oldThr << 1;
} else if (oldThr > 0)
// 如果capacity < 0 且 threshold > 0, 则 capacity = threshold
newCap = oldThr;
else {
// 如果capacity < 0 且 threshold < 0, 初始化table(都使用默认值)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的threshold
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 将旧table中的数据移到扩容后的table中
@SuppressWarnings({ "rawtypes", "unchecked" })
Node[] newTab = (Node[]) new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 如果旧table的桶中只有一个bin, 将bin直接剪切到新table中
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果旧table的桶是树形bin, 使用树复制方式
((TreeNode) e).split(this, newTab, j, oldCap);
else {
// 如果旧table的桶是线性链表bin, 使用链表复制方式
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
// 由于 存储位置 = Key.hashCode ^ (capacity-1), capacity扩大2倍后,key的hash值也会向左多取1位
// 若多取的最高为0, hash值保持不变; 若为1, hash值则扩大2倍
// 下面的代码就是将原来的链表, 根据扩大后的新hash值,拆分为两个链表,分别存储在新table中的不同桶中。
// lo 代表高位为0, hi 代表高位为1, tail 为链尾, head 为链头
if ((e.hash & oldCap) == 0) { //高位 == 0
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else { //高位 == 1
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将 lo 链表放到新table的 j 位置, 将 hi链表放置到新链表的 j+oldCapacity 位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; //原来索引位置 j
}
if (hiTail != null) {
hiTail.next = null;
//新的索引位置 = oldCap旧表的长度 + 索引位置 j
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
从上面源码可的扩容时,计算新的索引高位
是 0
保存到原来的索引位置;如果高位
是1
那么存储到原来索引 + 旧容量的数组长度的位置
如(旧值5 + 数组长度16 )
final void split(HashMap map, Node[] tab, int index, int bit) {
TreeNode b = this;
// Relink into lo and hi lists, preserving order
TreeNode loHead = null, loTail = null;
TreeNode hiHead = null, hiTail = null;
int lc = 0, hc = 0;
// 树形Bin的复制其实与线性链表bin很相似, 也是根据扩容后hash值的最高位, 分解成两个链表
// 区别在于分解后的两个链表, 如果 元素个数 < UNTREEIFY_THRESHOLD ,会将树转化为线性链表bin; 否则就会进行树化
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)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;
}
}
if (loHead != null) {
//UNTREEIFY_THRESHOLD = 6
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map); // 转化为线性链表
else {
tab[index] = loHead;
if (hiHead != null)
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);
}
}
}
列表的位置取模运算: (n - 1) & hash
第一次扩容:16
0000 0000 0000 0000 0000 0000 0000 1111 : 容量 :n = 15
0000 0000 0000 0000 0000 0000 0001 0011 : hash值 :hash = 19
取模:
0000 0000 0000 0000 0000 0000 0000 0011 : 结果 :n = 3第二次扩容:32
0000 0000 0000 0000 0000 0000 0001 1111 : 容量 :n = 31
0000 0000 0000 0000 0000 0000 0001 0011 : hash值 :hash = 19
取模:
0000 0000 0000 0000 0000 0000 0001 0011 : 结果 :n = 19可得:
扩容后最高位是0 ,保持在原来的位置;
扩容后最高位是1 ,保持在原来的位置 + 扩容前的容量;
六、获取元素 (get)
- get方法
public V get(Object key) {
Node e;
//根据 hash 和 key 获取 Node 结点,并返回节点的 value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
transient Node[] table;
final Node getNode(int hash, Object key) {
//临时变量储存tab数组
Node[] tab;
//first 临时变量获取第一个元素
//e 为临时变量储存在first元素不是所需元素的下一个元素
Node first, e;
//n为table的长度
int n;
K k;
//如果tab已经被初始化切table数组的长度大于0,在已知元素的查找位置上有元素则进入if判断
//否则返回null,即没有找到元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
/**
* 以下两个条件与
* 1、first.hash == hash: 头节点的hash值与待查找key的hash值相同
* 2、((k = first.key) == key || (key != null && key.equals(k))):key值相等
* 当这两个条件同时满足,说明头节点即是待查找的元素,直接返回头节点
*/
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 否则在后继节点中查找,e-当前遍历的节点
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果当前节点是红黑树节点,那么直接在红黑树中查找元素
return ((TreeNode)first).getTreeNode(hash, key);
//如果链表没有被树化,则使用链表的方式查询.
do {
//循环判断当前的临时变量e是否与所需元素相同
//相同则返回e元素,不相同则返回null
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
执行 TreeNode#getTreeNode
/**
* Calls find for root node.
*/
final TreeNode getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
执行此 TreeNode#find
方法,获取节点:
/**
* 这是TreeNode的内部方法
* 1、先比较hash值,看是在左子树找还是右子树找;
* 2、再判断key是否相等,如果相等就直接返回;如果不相等,那么就看那边不为空就在那边找;
* 3、如果左右节点都不为空的话,那么就要通过Comparable方法来比较是在左子树还是右子树查找
*/
final HashMap.TreeNode find(int h, Object k, Class> kc) {
HashMap.TreeNode p = this;
do {
// ph-遍历节点的hash值,dir-位置标志位,小于0,左子树查找,大于等于0,右子树查找,pk-遍历节点的key值
int ph, dir;
K pk;
HashMap.TreeNode pl = p.left, pr = p.right, q;
// 大于遍历节点大于查找key对应的hash值,在遍历节点的左子树查找
if ((ph = p.hash) > h)
p = pl;
// 大于遍历节点小于查找key对应的hash值,在遍历节点的左子树查找
else if (ph < h)
p = pr;
// key相等,说明已找到,直接返回遍历节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 左子树为空,在右子树查找
else if (pl == null)
p = pr;
// 右子树为空,在左子树查找
else if (pr == null)
p = pl;
// 如果实现了Comparable接口并且也比较出了大小,那就可以明确是在左子树还是右子树查找,
//如果无法实现的话,那么就在左子树或者右子树递归查找
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
// 小于左子树查找,大于等于右子树查找
p = (dir < 0) ? pl : pr;
// 右子树找到,直接返回
else if ((q = pr.find(h, k, kc)) != null) //递归查询,里面用到慢查找
return q;
// 右子树没找,在左子树查找
else
p = pl;
} while (p != null);
return null;
}
七、移除元素 (remove)
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
移出节点
final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
//获取节点 p,(n - 1) & hash 是节点对应的位置
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e; K k; V v;
//如果 hash 相等, key 相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p; //返回数组位置上的节点 node
else if ((e = p.next) != null) {
//p 的next 节点不为空,这是遍历链表上的节点 node
if (p instanceof TreeNode) //如果树查询节点,步骤在get方法中
node = ((TreeNode)p).getTreeNode(hash, key);
else {
//链表查询节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//删除节点,matchValue 验证是否匹配
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) //树移除
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p) //链表移除
tab[index] = node.next;
else
p.next = node.next;
++modCount; //修改次数加1
--size; //节点个数减1
afterNodeRemoval(node);
return node;
}
}
return null;
}
- 在树中移出节点
TreeNode#removeTreeNode
final void removeTreeNode(HashMap map, Node[] tab,
boolean movable) {
int n;
//tab 表 == null 或 length == 0 直接返回
if (tab == null || (n = tab.length) == 0)
return;
//获取位置
int index = (n - 1) & hash;
//获取根节点
TreeNode first = (TreeNode)tab[index], root = first, rl;
TreeNode succ = (TreeNode)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 || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode 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 sr = s.right;
TreeNode pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode 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 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;
}
//balanceDeletion :左旋,右旋 使树平衡
TreeNode r = p.red ? root : (root, replacement);
if (replacement == p) { // detach
TreeNode pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
//movable = true
if (movable)
moveRootToFront(tab, r);
}
八、初始化
当我们初始化HashMap并设置大小,大小不是 2 的幂会发生什么呢?
//初始化大小为 11
HashMap map = new HashMap(11)
- 源码做的事情如下:
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//initialCapacity 初始化大小
//loadFactor :加载因子 == 0.75
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;
//设置扩容阈值 threshold。就是决定是否扩容的边界值
//tableSizeFor 位运算得到最接近 2 的幂
//在 put 方法中会重新对阈值 threshold 进行修改
this.threshold = tableSizeFor(initialCapacity);
}
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;
//MAXIMUM_CAPACITY最大容量 2^30
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
根据上面的算法可以得到,其会返回 cap
也就大于cap且最接近的2的幂,如:cap = 12,返回 :16
前提:当输入 12
cap :12
n :cap - 1 = 11 : 主要是为了防止输入的值刚好是2的幂
n |= n >>> 1
0000 0000 0000 0000 0000 0000 0000 1011 : n = 11
0000 0000 0000 0000 0000 0000 0000 0101 : n >>> 1 = 5
------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1111 : n = 15
n |= n >>> 2
0000 0000 0000 0000 0000 0000 0000 1111 : n = 15
0000 0000 0000 0000 0000 0000 0000 0011 : n >>> 2 = 3
------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1111 : n = 15
n |= n >>> 4
0000 0000 0000 0000 0000 0000 0000 1111 : n = 15
0000 0000 0000 0000 0000 0000 0000 0000 : n >>> 4 = 0
------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1111 : n = 15
当执行:return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1
可得返回:n + 1 = 16