Jdk1.8集合框架之HashMap源码解析(详细解析红黑树)

本文深入解析Jdk1.8 HashMap,重点讲解其内部红黑树的实现,包括特点、内部字段、树节点构造及操作,如putTreeVal、treeify、旋转等。此外,还涵盖了HashMap的构造函数、put方法、hash函数以及扩容resize机制。
摘要由CSDN通过智能技术生成

HashMap特点

  • 不同步,支持null的键和值,put或get操作通常是常数时间。
  • Map接口的实现。
  • 去掉了Hashtable的contains(Object value)方法,保留containsKey和containsValue方法。
  • 使用Iterator而不是Enumration。

对HashMap还不太了解的同学,可以先看看这篇文章大致了解框架:HashMap简单入门

内部字段

// 默认初始长度为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

// 桶数组的最大长度为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

// 构造器为设置时,默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 当桶中的节点数大于8时,桶结构由单链表转化为一个既是红黑树又是双链表的结构
static final int TREEIFY_THRESHOLD = 8;

// 当桶中的节点数小于6时,树转单链表
static final int UNTREEIFY_THRESHOLD = 6;

// 只有桶位数组大小达到64时,才允许桶位树化,否则只是扩容
// 至少为4 * TREEIFY_THRESHOLD以避免resize和树化的冲突
static final int MIN_TREEIFY_CAPACITY = 64;

// 哈希桶数组,数组中保存的是链表或树节点。长度总是2的整数幂。
transient Node<K,V>[] table;

// HashMap将数据转换成set的另一种存储形式,这个变量主要用于迭代功能
transient Set<Map.Entry<K,V>> entrySet;

// Map中的键值对个数
transient int size;

// 结构性修改的次数,和迭代器的使用有关,对应fail-fast
transient int modCount;

// 扩容阈值,当table大小超过阈值时要扩容为2倍
int threshold;

// 负载因子,用来计算当前table长度下的容量扩容阈值:threshold = loadFactor * table.length
final float loadFactor;

内部类

a. 单链表节点
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 这个hash值只考虑键
    final K key; // final:说明节点的key不可变
    V value;
    Node<K,V> next; // 指向下一个节点,形成单链表

    Node(int hash, K key, V value, Node<K,V> 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;
    }
}
b. 树节点(难点)

这里的红黑树比较特殊,它的规则是:

  1. 根节点是黑色的。
  2. 红节点可以为左孩子,也可以为右孩子,一个节点可以同时有两个红色的左右孩子。(有些红黑树的实现要求红节点必须是左孩子)。
  3. 红节点的子节点必须是黑色。
  4. 每个节点到根节点的路径上黑色节点数量相同。

这里写图片描述

基本属性和构造方法

HashMap中的红黑树,同时也是一个双链表,parent、left、right和red字段构建了红黑树信息,而prev、next(next是基类HashMap.Node的字段)构建了双链表信息。树的root节点同时也是双链表的头结点,放在桶位数组上。这种双结构,使得红黑树操作比纯粹红黑树要复杂一些。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;   // 父节点
    TreeNode<K,V> left;     // 左孩子
    TreeNode<K,V> right;    // 右孩子
    TreeNode<K,V> prev;     // 前一个节点
    boolean red;    // true为红链接,false为黑链接

    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

    ...
}
putTreeVal 方法

HashMap的putVal方法会调用树方法putTreeVal来插入新的键值对。

该方法首先会查找树中是否有这个键,有的话就返回这个节点的引用,但注意,这里没有马上更新Value。查找的过程中,首先比较hash值,其次再比较Key,应该是考虑到比较int类型的速度更快。没有找到的话,会新建一个树的叶子节点,再调用树方法balanceInsertion插入新节点到红黑树中。

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;
    // 找到树根
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (TreeNode<K,V> p = root;;) { // p为当前和新元素比较的节点
        int dir, ph; K pk;
        // 根据hash值比较大小
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        // hash值相等时,如果键指向同一地址,查找结束
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        // hash值相等,比较两个键
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            // 键不可比较,或者比较结果为0时,在节点的左右子树查找该Key,找到就结束
            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;
            }
            // 比较两个对象的内存地址
            dir = tieBreakOrde
  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值