HashMap和源码笔记

HashMap这个容器不仅使用的多,同时知识点也很多,特别在jdk1.8引入红黑树,所以在这个容器上记下几笔笔记方便以后查阅。

储存结构

HashMap结构图

Node节点代码如下:

    static class Node implements Entry {
        final int hash;//索引
        final K key;//键
        V value;//值
        Node next;//链表下一个Node

        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) {
                Entry e = (Entry)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

HashMap是数组(称之为哈希桶数组)+链表/红黑树(jdk1.8后)来保存数据的,通过链地址法解决哈希冲突。

主要变量

    //默认初始容量:16,必须是 2 的整数次方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //最大容量: 2^ 30 次方
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默认加载因子的大小:0.75,可不是随便的,结合时间和空间效率考虑得到的
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //树形阈值:JDK1.8新增的,链表超过这个长度时使用红黑树而不再使用链表。必须>2
    static final int TREEIFY_THRESHOLD = 8;

    //非树形阈值:也是 1.8 新增的,小于该值红黑树变链表,要比 TREEIFY_THRESHOLD 小
    static final int UNTREEIFY_THRESHOLD = 6;

    //当哈希表中的容量大于这个值时,表中的桶才能进行树形化
    //否则桶内元素太多时会扩容,而不是树形化
    //为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
    static final int MIN_TREEIFY_CAPACITY = 64;
  
    //HashMap的值达到这个值时,进行扩容(这个值等于length*loadFactor,但并不是在初始化时就设定的,下文会进行说明)
    int threshold;
    //实际的扩容印子
    final float loadFactor;

HashMap的容量值必须是2的次方数,因为扩容时的hash算法涉及到取模运算 ,

key.hashCode()& (length-1)

倘若length是2的次方,比如二进制数1000(8),0001 0000(16),那么(length-1)是0000 0111(7),0000 1111(15),1的与(&)运算重复值少(因为0的与运算都是0),这样HashMap中元素位置会分布尽可能散一些(少重复)
这个思想在构造函数中调用的tableSizeFor()方法体现出来:

    //返回大于输入参数且最近的2的整数次幂的数  比如输入0000 1xxx xxxx xxxx,或者输入二进制0000 0000 0000 1010(十进制10),返回16
    static final int tableSizeFor(int cap) {
        int n = cap - 1; //0000 01xx xxxx xxxx,0000 0000 0000 1001
        n |= n >>> 1;    //对n无符号右移1位并进行位或运算:可以看到之前1的地方不变还是1,右移出来的1的位置也是1,也就是0000 011x xxxx xxxx,0000 0000 0000 1001->0000 0000 0000 1101
        n |= n >>> 2;    //对n无符号右移2为并进行位或运算:同上理得到0000 0111 1xxx xxxx,0000 0000 0000 1101->0000 0000 0000 1111
        n |= n >>> 4;    //0000 0000 0000 1111->0000 0000 0000 1111
        n |= n >>> 8;    //0000 0000 0000 1111->0000 0000 0000 1111
        n |= n >>> 16;   //同理 ,最后可让除最高位都变为1,这样再执行n+1就成了2的次方幂
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//0000 0111 1111 1111最后n+1就是0000 1000 0000 0000,另一个数是(16)0000 0000 0001 0000也就是比10大的最小2的n次幂
    }

主要方法

1.确定哈希桶数组索引位置

// 方法一,jdk1.8 & jdk1.7都有:
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 方法二,jdk1.7有,jdk1.8没有这个方法,但是实现原理一样的:
static int indexFor(int h, int length) {
     return h & (length-1);  
}

这里的hash算法本质上是以下3步:(结合上面tableSizeFor方法看)

  • 取key的hashCode值,h = key.hashCode();
  • 高位参与异或运算,h ^ (h >>> 16);
  • 取模运算,h & (length-1)。
确定索引.png

最后结果是0101=5,这也和前面数组长度必须是2的n次方幂联系起来,可以看到0的与运算都是0,若低位也是0,出现同一数字的概率会变大。

2.putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)

putVal
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //table是否为null
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//扩容
        if ((p = tab[i = (n - 1) & hash]) == null)//根据键值key计算hash得到插入的数组索引并判断是否为null
            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))))//key是否存在
                e = p;//key存在覆盖掉
            else if (p instanceof TreeNode)//key不存在,判断是否是treeNode
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);//红黑树插入键值对
            else {//key不存在,并且是链表,那么遍历准备插入
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);//插入链表
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//链表长度是否大于转换红黑树最低限制
                            treeifyBin(tab, hash);//转换红黑树
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)//是否扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3.扩容resize()

    final Node[] resize() {
        // 当前table
        Node[] oldTab = table;
        // 当前table的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 当前阈值
        int oldThr = threshold;
        // 新的容量值和阙值
        int newCap, newThr = 0;
        /*
        1. resize()函数在size > threshold时被调用。oldCap大于 0 代表原来的 table 表非空,
           oldCap 为原表的大小,oldThr(threshold) 为 oldCap × load_factor
        */
        if (oldCap > 0) {// 1:若旧table容量已超过最大容量,更新阈值为Integer.MAX_VALUE(最大整形值),这样以后就不会自动扩容了。
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }//新的容量为旧的两倍,
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 阈值翻倍
                newThr = oldThr << 1; // double threshold
        }
        /*
        2. resize()函数在table为空被调用,table创建了但没添加元素。oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为
           HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
           或 HashMap(Map m),导致 oldTab 为 null,oldCap 为0, oldThr 为用户指定的 HashMap的初始容量。
      */
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        /*
        3. resize()函数在table为空被调用。oldCap 小于等于 0 且 oldThr 等于0,用户调用 HashMap()构造函数创建的 HashMap,所有值均采用默认值,oldTab(Table)表为空,oldCap为0,oldThr等于0,
        旧容量、旧阈值都是0,说明还没创建哈希表,容量为默认容量,阈值为 容量*加载因子
        */
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            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;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //单个节点
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order 链表
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {
                            next = e.next;
                            // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行判断分成两种情况
                            //最高位==0,这是索引不变的链表。
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //最高位==1 (这是索引发生改变的链表)
                            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;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // rehash 后节点新的位置一定为原来基础上加上 oldCap,
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

下文扩容更多细节来自 美团技术博客-Java 8系列之重新认识HashMap

经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

hashmap确定索引.png

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:


最高位0,1确定索引.png

只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:


0,1确定索引详细例子.png

你可能感兴趣的:(HashMap和源码笔记)