Bootstrap

jdk 源码系列之HashMap

jdk 源码系列之 HashMap

JDK 源码系列

前言

了解 HashMap,理解扩容机制、数据结构、阈值以及初始化机制,对使用、优化等有所裨益。

继承关系

1
2
3
public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
}

从这段代码,我们可知,HashMap 继承 AbstractMap 同时实现 Map 类。换句话说,Map 作为顶级父类,只定义方法,实现由子类实现,还继承 AbstractMap 那也是说,也可以调用 AbstractMap 里面的方法。

这里先摁住,还不清楚 HashMap 调了 AbstractMap 里面哪些方法。

HashMap

点进去先看看是数据结构是什么。

数据结构


/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node implements Map.Entry {
    final int hash;
    final K key;
    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;
    }
}

向上继承 Map.Entry 接口,并实现了 getKey getValue toString hashCode setValue equals 这六个方法。之后自己在实现一个链表 Node 的内部类,这个链表的节点都保存三个东西,hash key value 这三个变量。

接下来,我们从创建一个 HashMap 入手。

创建 HashMap

通常来讲,HashMap 有三种 new 的方式。


Map first = new HashMap<>();

Map second = new HashMap<>(12);

Map third = new HashMap<>(first);

Map fourth = new HashMap<>(12 , 1);

第一种是无参构造,第二种是传入一个 int 值,第三种是允许传入一个 Map 实现的任何子类,第四种是两个 int 类型。


/**
 * Constructs an empty HashMap with the default initial capacity
 * (16) and the default load factor (0.75).
 */
// first 的构造
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * Constructs an empty HashMap with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
// second 的构造
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * Constructs a new HashMap with the same mappings as the
 * specified Map.  The HashMap is created with
 * default load factor (0.75) and an initial capacity sufficient to
 * hold the mappings in the specified Map.
 *
 * @param   m the map whose mappings are to be placed in this map
 * @throws  NullPointerException if the specified map is null
 */
// third 的构造
public HashMap(Map m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

/**
 * Constructs an empty HashMap with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
// fourth 的构造
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;
    this.threshold = tableSizeFor(initialCapacity);
}

这里提到三个变量,分别 initialCapacityloadFactorthreshold

看中文意思,分别是初始化容量、负载因子、阈值。其中负载因子,也就是 loadFactor 默认是 0.75f


/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

在 third 这个构造函数里面,有个 putMapEntries 的方法。


    /**
     * Implements Map.putAll and Map constructor.
     * 构造方法的时候就全部添加进去 hashMap
     * @param m the map 传入的Map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion). 构造使用到就是 false 非构造就是 true
     */
    final void putMapEntries(Map m, boolean evict) {
        // Map 的大小
        int s = m.size();
        if (s > 0) {
            // 由于是构造调用的,table = null
            if (table == null) { // pre-size
                // 在调用次方法的时候,先赋值了 loadFactor = 0.75。所以ft = (Map 长度 / 0.75) + 1.0
                float ft = ((float)s / loadFactor) + 1.0F;
                // 判断是否超出 MAXIMUM_CAPACITY = 1 << 30 也就是 2 的30次幂大小
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                // 起始 threshold = 0,这个也在 fourth 的构造里面使用到了,下面有解释
                if (t > threshold)
                    // 如果 t 的大小在 2 的(n-1)次幂 与 2 的 n 次幂之间,则 t 的大小为 2 的n次幂。不懂没关系,下面有解释、举例
                    threshold = tableSizeFor(t);
            }
            // Map的长度 是否 大于 刚刚 tableSizeFor 之后的长度
            // 首次是不会大于这个值的
            else if (s > threshold)
                // 扩容,后面对这个方法讲解
                resize();
            
            // 将 Map 的子类对象 放进HashMap
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                // 先通过位运算减少 key 的冲突,在放进去
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

putMapEntries 这个方法就是 HashMap 在构造的时候,放进 Map 的对象,那么就会将他是所有 k v 都放进去,然后初始化长度,这个长度是当前 Map 长度靠近 2 n次幂 长度。比如

  • Map 7,则HashMap 初始化8,

  • 如果是12 则是 16,

  • 如果是16 则是 16.

之后判断是否需要扩容,由于是首次,所有不需要,接着就是放进 HashMap。先通过位运算计算 Hash 减少冲突,在pull。


    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     *
     * 通过位运算减少 Hash 冲突
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在 fourth 这个构造函数里面,还有一个这 tableSizeFor 的方法,上面有用到。


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;
}

进行了位运算,由于前面好保证了 cap 非负数。

  • »> 右移,高位补零。比如 0001,使用 »> 1 之后就是 00001,对其长度,则1去掉。

  • **** 或操作,不同取1,相同1取1,相同0取0

tableSizeFor 方法里面的意思就是,如果 cap 介于 2 (n - 1)次幂 与2 n次幂之间,cap 的值为 2 n次幂 - 1 大小。


int n = 18;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n); // 31

int n = 16;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n);  // 31

int n = 14;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n); // 15

最后只要算的 n 非负数,且不大于 int 类型最大值,则 n + 1。也就是说这个初始容量,无论设置什么数字,都会是 2 的倍数。

HashMap 添加

我们常常使用 kv 的形式存储。


    map.put("sin", "sy");

点进去看看,HashMap 如何存储。


    // 首先来到父类定义的
    V put(K key, V value);


    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with key, or
     *         null if there was no mapping for key.
     *         (A null return can also indicate that the map
     *         previously associated null with key.)
     * hashMap 的添加
     */
    // 然后实现父类定义的方法
    public V put(K key, V value) {
        // hash 方法上面有提到作用
        return putVal(hash(key), key, value, false, true);
    }

pull value 的时候,传入先经过位运算的 key 的 hashCode,然后传 key value、false,true


    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value 这个值在pull 的时候是false
     * @param evict if false, the table is in creation mode. 这个值在pull 的时候是 true
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 初始化两个容器 一个 k v 数组链表、一个是链表
        Node[] tab; Node p; int n, i;
        // 第一次这里是null,所以 n = (tab = resize()).length;
        if ((tab = table) == null || (n = tab.length) == 0)
            // resize 下面有讲解
            // 是否扩容处理,没有扩容,则是当前长度,有则是之前的两倍
            n = (tab = resize()).length;
        // 因为数组从0开始计算,所以是 n - 1 取模,确定值放置在数值链表位置
        // 如果当前数组链表为null,同时 p = 链表table
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 创建一个链表
            // 没有key值,就给你创建一个
            tab[i] = newNode(hash, key, value, null);
        // 有值的情况下
        else {
            // 数组链表头有数据
            // 初始化 链表 泛型k
            Node e; K k;
            // hash 值一致且key值的地址也一样 或者 key不为null 且key的值相同
            // 大概就是相同key,替换value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果 p 是 树结构
            else if (p instanceof TreeNode)
                // 按树的形式 pul 进去
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            // 值不相同,也不是树结构,且链表头还相同,同一节点底下还有链表
            else {
                // 向下遍历
                for (int binCount = 0; ; ++binCount) {
                    // 链表下一节点为null
                    if ((e = p.next) == null) {
                        // p的下一节点指向创建一个新的链表
                        p.next = newNode(hash, key, value, null);
                        // 当链表的长度大于等于7,因为从1开始,也是说链表头有值,所以才从一开始。另外这是 ++1 性能比 i++好,i++比起++i多了一个创建临时变量(放入数据栈)的步骤,因此效率低一些
                        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,建立映射,kv
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // onlyIfAbsent 如果是 true 不改变现有的值,false 改变,或者 oldValue == null
                if (!onlyIfAbsent || oldValue == null)
                    // 完成关联
                    e.value = value;
                // 尾插,链表直接插到当前链表的最后一个位置上
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 快速失败机制,比较当前操作次数与记录次数,是否不一样
        ++modCount;
        // 因为完成一个 pulValue 所以增加一次次数,同时再次比较是否来到阈值
        if (++size > threshold)
            // 来到阈值,2倍扩容容器
            resize();
        // true 如果是头结点则删除
        afterNodeInsertion(evict);
        return null;
    }


    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     * 具有初始化容器大小、2倍扩容容器等功能
     * @return the table
     */
    final Node[] resize() {
        // 首次 table 是null
        Node[] oldTab = table;
        // 判断 oldCap 是否为 null 为null 则为0,反之则是容器长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 阈值 首次是0
        int oldThr = threshold;
        // 新容器容量0 新容器阈值0
        int newCap, newThr = 0;
        // 容量大于0
        if (oldCap > 0) {
            // 大于等于最大能容纳的长度
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 阈值等于最大能容纳的长度
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 容器长度是否大于默认16,且新容器是旧容器的两倍大小且不大于最大容纳长度,
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 新阈值也是旧阈值的两倍大小
                newThr = oldThr << 1; // double threshold
        }
        // 旧阈值 大于 0
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 新容器长度 = 旧阈值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 首次 pull的时候
            // 新容器长度 = 16
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 新阈值等于 负载因子 0.75 * 默认长度 16 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 这个判断只有在第一次使用构造方法传入一个 Map 才会用到,且这个Map 有值,其他是都基本走不到这里
        if (newThr == 0) {
            // 容器长度 * 负载因子 0.75
            float ft = (float)newCap * loadFactor;
            // 只要不大于最大容纳的长度,则 newThr = ft
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 赋值到类变量里面,基本确定阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 新数组链表 等于 新容器的长度
        Node[] newTab = (Node[])new Node[newCap];
        // 赋值到类的变量里面
        table = newTab;
        // 旧容器不为null
        // 这是是首次 oldTab 为 null 不走这里,比如第一次 pull 构造方法放入map的子类
        // 之后 pull 这里都会走一遍
        if (oldTab != null) {
            // 链表循环指向下一个节点
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                // 先赋值,当前数组链表的链表给 e 链表且不为null
                if ((e = oldTab[j]) != null) {
                    // 设置数组链表为null
                    oldTab[j] = null;
                    // 链表的下一节点为null
                    if (e.next == null)
                        // 只有数组链表头有数据,子节点没有数据
                        // hash 取模,这个模的长度是数组的长度,e 接到新的容器底下
                        newTab[e.hash & (newCap - 1)] = e;
                    // 判断 e 是否是树结构
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    // e 不是树结构,且数组链表的子节点也有数据
                    // 这里应该是计算不同hash的节点,到不同链表上
                    // 然后迁移到新的容器里,对应的数组链表的位置
                    else { // preserve order
                        // 
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        // 下一节点
                        Node next;
                        do {
                            // e 指向下一节点
                            next = e.next;
                            // e 的hash 取模 oldCap,分散。
                            if ((e.hash & oldCap) == 0) {
                                // 第一次为null
                                if (loTail == null)
                                    // loHead 是第一值
                                    loHead = e;
                                else
                                    // loTail的下一节点 指向 e的下一节点
                                    loTail.next = e;
                                // 不是最后一个位置
                                loTail = e;
                            }
                            // 
                            else {
                                // 和上面的步骤差不多
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 这里是原来数组链表的长度
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 2倍扩容之后的数组长度,换句话说,之前的数据,如果 hash 取模之后不等于,之前最后面的位置,则一律变成之前的位置 加上 旧数组长度的。
                        // 如果之前是 2 数组链表长度是 8,之后这个位置来到 10
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

总结下,在 pulValue 的时候做了什么。

上面,少了链表如何转红黑树。接下来,我们继续看看


    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node[] tab, int hash) {
        int n, index; Node e;
        // 链表长度是否小于 64,换句话说,树长最长只有64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 当链表长度,来到 8 的时候也会判断是否扩容
            resize();
        // 确定位置
        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);
        }
    }


    /**
     * Forms tree of the nodes linked from this node.
     */
    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;
            // 起始 red = false,当 = false 时候就是 黑色,同时根节放入值,只有首次才会走到
            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);
    }

转红黑树的过程中,首先会进行一次判断扩容处理,之后先变成一个个的树节点,在变成红黑树。

其中 red = false 时,为黑,反之则是红。通过左右旋,使其父节点变成黑色,子节点为红色,完成一次平衡处理。

图如下:

首次,root节点就是只有一个,所以直接是黑。当当前树高相差为3,会完成一次自旋,使其平衡,如果是不平衡的节点,则会变成红色,平衡的节点则为黑色。

HashMap 的数据结构图解

从上面,我们基本了解了,hashMap是怎么扩容的,如何链表转红黑树、如何定位位置。接下来我们看看HashMap的数据结构是如何。

起始的结构是由数组链表里面且是一个kv结构。头部是一个数组,k的位置是一个 hash 取模(位扰动之后的hash)之后,确定的位置。

后来当,hash冲突变多的时候,数组底下的链表就是存储 hash 冲突之后存储的位置,这个长度大于8的时候转红黑树,包括头,也就是数组,底下的链表长度是7。

优化以及使用 HashMap 的建议

既然读源码,就是为了更好的了解这个东西,只为读而读,为面试而读,这个源码读的没什么意思。学完得要有自己体会吧~

声明

作者: Sinsy

本文链接:https://blog.sincehub.cn/2020/10/31/jdk-hashmap/

版权声明:本文为博主原创文章,遵循 版权协议,转载请附上原文声明。

如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com

引用

  • [1]