菜鸟分析JDK1.8 HashMap源码

真的是菜鸟分析,大佬勿喷
分析的并不详细,看之前你要对hashmap的基本结构有一定的了解

首先看在hashmap中使用到的两个函数

// 找到指定整型的最高为1的bit位对应的值
// 比如13 -> 01101 求出的结果为 8 -> 01000 
public static int highestOneBit(int i) {
	/*
	* 按照一般的思路,如何求出最高位,直接循环判断是否等于0就可以了,这种方式的
	* 缺点其实非常的明显,循环次太多了
	* 下面这种方式的思想在于利用最高位的1去将其后面所有的非1的bit位都变成1
	* 然后现在的i最高位之后的位就是全1了,那么i - (i >>> 1)就能求出最高位代表的值了
	* 这里的移位是>>,也就是有符号的,所以所有负数求出来结果都是一样的
	*/
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    // 无符号右移,不管是正数还是负数,最高位都补0
    return i - (i >>> 1);
}
// 找到指定整型的最低bit位对应的值
public static int lowestOneBit(int i) {
	/*
	* 利用原码与补码的关系求出了最低位的代表的值
	* 补码会对原码进行取反,取反之后得到了反码
	* 反码与原码进行&操作会得到0,补码在反码的基础上进行了+1
	* 原码的最低位的位置对应在补码中的位置也就为1,就得到了结果
	*/
    return i & -i;
}

1.put()和putVal()方法
该方法就是对应的put操作

public V put(K key, V value) {
	// 直接调用了putVal(),调用之前计算出了hash()值
    return putVal(hash(key), key, value, false, true);
}

// 这个计算hash值的函数其实还是比较特殊
static final int hash(Object key) {
    int h;
    // 并不是直接将key的hash值进行返回,而是将hash值的高16位和低16位进行了^操作
    // 这样有什么好处呢?这样使高位也参与了hash值的计算,冲突率会降低
    // 我们在&的时候因为数组的长度的原因大多数时候只利用到了hash值的低位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab就是代表当前map中用来存放数据的数组
  	// p就是代表的每个数组下标位置存放的链表的头节点
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果此时数组还没有被初始化,需要先进行初始化
    // hashmap中的数组的初始化是延迟初始化,并不会在构造函数中马上初始化
    // 而是在第一次使用的时候进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*
	* 如果当前插入的key的hash值对应的下标位置为空,直接放入
	* 注意这里计算数组下标使用的是&操作,而不是%操作
	*/
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 先判断hash值是否相同,hash值不同两个key一定不同
		// 相同的情况下还要判断地址或者equals是否相同,hash相同并不能说明两个对象相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果相同的话,直接替换原来的值
            e = p;
        else if (p instanceof TreeNode)
        	// 当前链表变成了红黑树
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        	// 直接尝试插入
            for (int binCount = 0; ; ++binCount) {
                // 如果e==null,说明链表上没有相同的key
                if ((e = p.next) == null) {
                	// 直接进行插入,注意这里是尾插
                	// 在JDK1.7的HashMap中使用的是头插
                    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;
            }
        }
         // map中存在相同的key
        if (e != null) {
            V oldValue = e.value;
            // onlyIfAbsent:为true时,只有value不存在时才插入,否则不进行插入
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //该方法在hashMap中是一个空方法,该方法主要作用于LinkedHashMap
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 判断是否需要扩容
    if (++size > threshold)
        resize();
    // 该方法在hashMap中是一个空方法,该方法主要作用于LinkedHashMap
    afterNodeInsertion(evict);
    return null;
}

为什么计算下标使用&而不是%?
其实这个问题的答案很简单,当然是因为更快
虽然变快了,但是其对数组长度的要求相对于%来说要严格的多。%不管数组的长度是多少,计算出来的值一定是小于数组长度且平均的,但是&不行。
如果数组的长度是9会出现什么情况呢?9的二进制对应的是01001,如果对9进行&操作,最终得到的结果只能有4种情况,也就是对应到4个下标位置,数组的空间并没有被均匀使用,导致冲突变高。所以HashMap使用了&之后,对于数组的长度要求必须是(2^n-1),这样就能确保&操作之后数组下标均匀。

2.get()方法
get()方法比较简单就直接跳过吧

3.resize()方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	// oldCap大于0,说明了不是第一次调用resize()
    	// 数组长度大于最大值,不进行扩容了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新数组的容量等于旧数组容量的两倍,依然要满足2^n
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // double threshold
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) 
    	// 这种情况是第一个调用resize()
    	// HashMap的某一个构造函数会将初始的容量是放置在threshold中的
    	// 为什么要这样放,主要也是延迟初始化
        newCap = oldThr;
    else {               
    	// zero initial threshold signifies using defaults
    	// 这种情况下就是第一次进行resize()且使用的是默认构造
        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;
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 如果oldTab不等于null,说明不是第一次调用resize()需要将原来的数组中的数据放到新数组中
    if (oldTab != null) {
    	// 遍历老数组的每一个下标位置
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 判断每一个下标位置是否存在链表
            if ((e = oldTab[j]) != null) {
            	// 将旧的置为空,链表的头位置已经放置在e中了
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                	// 红黑树的情况
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                	// 给老数组中的每一个节点计算新的位置
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 因为每次扩容都是对容量的翻倍
                        // 只需要判断hash值相对于数组长度多出来的那一位是0还是1
                        // 根据其是0或者是1选择放置在新数组的高半段还是低半段
                        // 低半段也就是相当于老数组的原来的位置
                        // 因为出现0或者1的的概率是完全随机的,所有也有很好的分布情况
                        if ((e.hash & oldCap) == 0) {
                        	// 0则放置在低半段
                            if (loTail == null)
                                loHead = e;
                            else
                            	// 注意这里的放置方法,在JDK1.7的时候,resize()会导致链表反转,这里就不会了
                                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;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

未完待续

发布了162 篇原创文章 · 获赞 44 · 访问量 8844

猜你喜欢

转载自blog.csdn.net/P19777/article/details/103561007