JDK1.8 HahMap 红黑树源码学习

本文详细解析了Java HashMap在1.8版本中的扩容条件,即当table数组长度达到64且链表长度大于8时触发扩容。同时,深入探讨了HashMap在转换为红黑树时的插入逻辑,包括如何寻找插入位置、如何保持红黑树平衡以及在Key未实现Comparable接口时的处理方式。通过对put方法的分析,理解了红黑树在插入元素时的平衡策略,这有助于更好地理解和使用HashMap。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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()方法就比较容易理解了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值