1 JDK 1.8 hashmap扩容条件 table数组长度达到64 且 链表长度大于8
2 可以使用如下代码来调试
public class Key {
protected int value;
public Key(int value) {
this.value = value;
}
@Override
public int hashCode() {
return value / 20;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;// 内存地址相等一定时相同对象
if (obj == null || obj.getClass() != getClass())// obj是Key的子类 返回也为真 getClass()==obj.getClass() 必须是相同的类
return false;// 能调用equals方法 说明this一定不为空,obj是否为Key对象
Key key = (Key) obj;
return key.value == value;
}
@Override
public String toString() {
return "v("+value+")";
}
}
public static void main(String[] args) {
HashMap<Integer, Object> map = new HashMap<>(64);// 初始容量设置为64
for (int i = 0; i < 10; i++) {
map.put(new Key(i), i);// 插入到第9个元素时开始转红黑树
}
}
3 插入元素代码学习
代码块1
当节点类型为treeNode时,将key放到红黑树上
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
代码块2
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;// 是否扫描搜索过了
// table[index]处的节点不一定为红黑树的根节点,如果不是调用root方法找到红黑树根节点
// 如果是代码块1 调用putTreeVal方法,因为在putTreeVal方法的最后调用moveRootToFront会把根节点放到table数组上
// 所以代码块1中 ((TreeNode<K,V>)p).putTreeVal 节点p就是根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;// dir 搜索红黑树的方向
//搜索红黑树 优先比较hashcode来查找,红黑树是二叉树,有序的
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;// hash值相等,同时equals 直接返回当前节点
else if ((kc == null && (kc = comparableClassFor(k)) == null)
|| (dir = compareComparables(kc, k, pk)) == 0) {
// 这个if判断有点恶心(JDK代码真精简啊)
// (kc == null && (kc = comparableClassFor(k)) == null)这是一个整体块,第一次进行比较时,kc=null,此时执行后面的代码 comparableClassFor方法判断k是否实现了comparable接口
// 如果k实现了,kc!=null,(kc== null &&(kc = comparableClassFor(k)) == null) 这个结果为false,需要执行后面的代码,dir = compareComparables(kc, k, pk)) == 0
// 此时k 与 pk具备可比较性,比较结果dir=0 则执行下面的if代码块 执行扫描,dir!=0则不进行扫描 按照dir 执行下一此的for循环查找
// 另外一种情况,k没有实现接口,不具备可比较性 测试dir=0 直接执行if代码块来扫描
// searched来进行扫描的判断,这里的扫描会从当前节点向下扫描他的所有子节点,如果没有if判断 第二次的for循环 上面的的if else if 都不会执行,会执行最后这个else if
// 此时的p 节点为上次扫描的节点的(左或右)子节点,再次经行扫描 很显然扫描重复了,因为父节点扫描时 已经把所有的子节点扫描过一边了 没必要再次扫描 所以加了searched判断
if (!searched) {
TreeNode<K,V> 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;
}
// 扫描没有相等的值,执行tieBreakOder,这个方法返回的dir永远不为0 参见代码块3
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;// p作为父节点临时变量保存 插入节点时需要设置父节点
// dir<0 p=p.left 如果p.left(或p.right)不为空,if里面代码块不会执行,执行下一次的for循环,
// dir=0的情况 是要插入的key不具备可比较性,kc = comparableClassFor(k)) == null 结果为true 执行else if内的扫描
// 如果扫描没有equal的节点,则从当前节点开始,一路向左找,直到找到为空的节点插入,
// 这里之前有疑惑 一直向左递归找到空节点然后插入,红黑树是否会不平衡
// 因为插入节点后,红黑树会根据节点的颜色来平衡红黑树,后面的balanceInsertion方法来平衡,所以红黑树一定会保存平衡
// 如果p=null 则找到了要插入的节点位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> 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<K,V>)xpn).prev = x;
// balanceInsertion方法根据节点的红黑属性来平衡红黑树,
// 关于红黑树的平衡原理(通过旋转 染色来平衡),这里不在进行展开说明,
// 最终得到的红黑树满足红黑树的5大特性即平衡了
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
关于代码块2中 if (!searched) { ...} ,这一块的扫描说明,在我们测试代码中Key没有实现Comparable接口,添加元素时一定时按照dir = tieBreakOrder(k, pk); 来添加元素,在我们使用的测试代码 中 Key k1 = new Key(1); Key k2 = new Key(1),我们自己重新定义了equals方法,k1和k2 是equals的, 所以
map.put(new Key(1)); map.put(new Key(2));
map.put(new Key(3)); map.put(new Key(4)); ......
当我们调用map.put(new Key(2));时此时应该覆盖原来的key(2);如果我们在加入元素的时候没有扫描,直接根据dir = tieBreakOrder(k, pk);加入元素,获取元素时有可能会获取不原来的key(2)元素,因为System.identityHashCode(a);这个方法得到的数据是随机的(没有重写hashcode方法,这个方法返回值等于原hashcode方法返回值).
代码块3
// 用于不可比较或者hashCode相同时进行比较的方法, 只是一个一致的插入规则,用来维护重定位的等价性。 这套规则来维护红黑树的插入规则
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
// 查看System.identityHashCode()方法JavaDoc可得,该方法返回给定对象的哈希代码与默认方法hashCode()返回的哈希代码相同,无论给定对象的类是否重写hashCode()。
// 即Object方法原生的hashcode方法,与ystem.identityHashCode( )方法返回值相同
// 这个方法跟对象的内存地址有关,两个完全不同的对象的调用System.identityHashCode(a) 有可能会相同 测试代码如下(所以即使没有重写hashcode方法 不同的对象hashcode也可能会相同)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
// 测试代码
public static void main(String[] args) {
HashMap<Integer, Object> map = new HashMap<>();
int count = 0;
while (true) {
count++;
Object o = new Object();
Integer i = System.identityHashCode(o);
if (map.containsKey(i)) {
System.out.println("FBI WARNING code:" + i + " count:" + count+"hashcode " +o.hashCode());
} else {
map.put(i, o);
}
}
}
代码块4 (有点绕)
// 从当前节点查找,并向下查找所有子节点 来查找是否有equals的节点
// 由代码块2得 执行查找方法有两种情况
// 情况1 具备可比较性 kc!=null 且在putTreeVal 方法中dir=compareComparables(kc, k, pk))==0为true
// 情况2 不具备可比较性 kc=null
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// h为插入key的hashcode
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> 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)
// 具备可比较性 比较结果dir不为0 则根据dir来确定方向 执行下次循环
p = (dir < 0) ? pl : pr;
// 继续向下执行说明 具备可比较性且比较结果dir=0 或者 不具备可比较性(没实现Comparable接口)
// 则依次扫描当前节点的左右子节点
// 下面else if和 else这两种情况等价于
// else if ((q = pr.find(h, k, kc)) != null) return q;
// else if ((q = pl.find(h, k, kc)) != null) return q;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else // 从pr右子节点查找没有找到对应的equals节点,令p=pl 继续执行do循环 扫描左子节点
p = pl;//这个循环设计有点妙啊。。。
} while (p != null);
return null;// 所有节点扫描完 没有找到equals节点,返回true
}
代码块5
/**
* 将root放到头节点的位置
* 如果当前索引位置的头节点不是root节点, 则将root的上一个节点和下一个节点进行关联,
* 将root放到头节点的位置, 原头节点放在root的next节点上
*
* 原理解释
* 因为treeNode节点维护了prev前驱节点 next 后驱节点属性 整个转移过程调整的是prev 和next属性
* 而节点的左右子节点 left right属性没有调整
* 可以把这个红黑树看作为一个双向链表,我们此时的操作是要将root节点放到双向链表的头节点处!!!
* 按照以上思路来看源码,发现代码逻辑很清晰。
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
// 校验root是否为空、table是否为空、table的length是否大于0
if (root != null && tab != null && (n = tab.length) > 0) {
// 计算root节点的索引位置
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点
if (root != first) {
Node<K,V> rn;
// 将该索引位置的头节点赋值为root节点
tab[index] = root;
TreeNode<K,V> rp = root.prev; // root节点的上一个节点
// 下面两个操作是移除root节点的过程(从双向链表中移除root节点)
// 同时改变root节点的前后节点的next和prev属性,切记此时是从双链表中的移除中间的root节点
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
// 把root节点放到双向链表的头部,注意非空条件判断
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
// 检查树是否正常(包括左右子节点的父节点指针是否正常,以及红节点的子节点不能为红节点(红黑树特性)的校验,关于assert关键字的使用,这里就不做过多的说明了)
assert checkInvariants(root);
}
}
红黑树得添加节点看懂了,再去看get(key)方法,和resize()方法就比较容易理解了。