HashMap的put方法源码解析_JDK8

package demo.JavaJdk8;

import java.util.HashMap;
import java.util.Map;

/**
 * @author Xch
 */
public class MapDemo{

    public void putDemo(){
        Map mapDemo=new HashMap<>(2);

        mapDemo.put("one",1);
        
        Integer one=mapDemo.get("one");
        
        System.out.println(one);
    }

}

前戏

上面的代码是我们平时对HashMap最简单的使用:

1、new 一个实例对象。

2、之后调用 put() 方法为集合添加一个键值对。

3、之后我们再调用 get() 方法得到一个键的值。

无论是 JDK7 还是 JDK8 都是这样的使用,但 JDK8 对 HashMap 进行了更加“优美”的优化。

以下所有代码、解读都基于 JDK8 ,为了方便查看源代码,我是用了IntelliJ_IDEA开发工具。

一、Map mapDemo=new HashMap<>(2)------带上taotao

HashMap的初始化,很简单的赋予这个HashMap一个初始化长度为2。

我们看看数组初始化做了什么?

让我们按键Ctrl+鼠标放在HashMap<>(2)上,之后鼠标左击,便会进入HashMap的默认构造函数源码中:

/**
     * 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.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

之后按键Ctrl+鼠标放在this上,之后鼠标左击(同上查看源码操作,以后不再详解),便会进入HashMap的另一个构造函数源码中:

/**
     * 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
     */
    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);
    }

这里我们需要驻足,详细解析HashMap这个构造函数:

两个参数:初始化数组大小(initialCapacity)和 加载因子(loadFactor默认值为0.75

如果:初始化数组大小(initialCapacity)< 0

           抛出一个IllegalArgumentException异常。

如果 :初始化数组大小(initialCapacity)> MAXIMUM_CAPACITY  ( = 1 << 30 = 1*2^30 )

            初始化数组大小(initialCapacity)= 2^30

如果:加载因子(loadFactor)<= 0

           抛出一个IllegalArgumentException异常。

之后为加载因子(loadFactor)赋值。

最后一行代码:this.threshold = tableSizeFor(initialCapacity);----------(深入):

查看tableSizeFor()方法源码:

/**
     * Returns a power of two size for the given target capacity.
     */
    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 最近的并且没有变小的 2 的幂次方数,比如传入10 返回 16,就是这么神奇!

给出算法的过程:

cap = 10;

n = 10 - 1;

n = 9; (1001)

1001 >>> 1 = 0100;
1001 或 0100 = 1101;

1101 >>> 2 = 0011;
110 或 0011 = 1111;

1111 >>> 4 = 0000;
1111 或 0000 = 1111;

1111 >>> 8 = 0000;
1111 或 0000 = 1111;

1111 >>> 16 = 0000;
1111 或 0000 = 1111;

1111 == 15;
15 + 1 = 16;

 threshold便是HashMap的阈值,但此时的这个阈(yu)值,只是初始化时给定的,不是最终的。

HashMap的初始化到此结束!

二、mapDemo.put("one",1)------she一个

1、查看put()的源码

HashMap的put方法源码解析_JDK8_第1张图片点击进入查看HashMap的put()的源代码:

/**
     * 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.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

 先查看hash(key)的源码:

/**
     * 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.
     */
    static final int hash(Object key) {
        int h;
        // 更好的均匀散列表的下标
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

继续查看putVal(hash(key), 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
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        
        // 如果全局变量table为null,或者长度为0,那么需要为tab初始化数组。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // 如果通过hash值计算出的下标的地方没有元素,根据给定的key 和 value 创建一个元素
        if ((p = tab[i = (n - 1) & hash]) == null) // <--1-->
            tab[i] = newNode(hash, key, value, null);

        // 如果hash冲突(新的hash我们称之为 新hash,被冲突的已经存在的hash我们称之为 旧hash.
        // 其他元素也照此称之)
        else {
            Node e; K k;
            // 如果新hash和 旧hash 值相等并且(旧key和新key相等 (地址相同,或者equals相同)),
            // 说明新key和旧key相同,那么我们把旧p赋值给e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果p的类型是树类型,则让红黑树追加这个键值对,赋值给e
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            // 如果key不相同,且hash冲突,且不是树,则只能是链表
            else {
                // 循环链表
                for (int binCount = 0; ; ++binCount) {
                    // 如果链表元素的next为空,表明链表到尾巴了
                    if ((e = p.next) == null) {
                        // 创建新节点,赋值给已有的next属性上.(把新键值对追加到链表尾巴上)
                        p.next = newNode(hash, key, value, null);
                        // 如果链表长度大于7,也就是等于8时
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 则将链表改为红黑树 (jdk8新特性)
                            treeifyBin(tab, hash); // <--2-->
                        // 结束循环
                        break;
                    }
                    // 如果新hash值和next的hash值相同且(key也相同)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //  结束循环
                        break;
                    // 如果新hash值不同或者key不同。则将next值赋给 p,开始下次循环
                    p = e;
                }
            }
            // 综合上面所有的判断,如果e不是null,那么该元素已经存在了(也就是新旧的key相等)
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 这里的onlyIfAbsent 是false.如果 value 是null
                if (!onlyIfAbsent || oldValue == null)
                    // 将新值 替换掉 老值
                    e.value = value;
                afterNodeAccess(e);
                // 返回被替换掉的旧值
                return oldValue;
            }
        }
        //如果e== null,迭代器的计数加一,为迭代器遍历使用
        ++modCount;
        // 如果数组长度大于了阀值
        if (++size > threshold)
            // 重新散列
            resize(); // <--3-->
        afterNodeInsertion(evict);
        // 返回null
        return null;
    }

2、上面的代码有三个函数特别详解

<1>: tab[i = (n - 1) & hash];

使用数组长度减一 &运算 hash 值。这行代码就是为什么要让前面的 hash 方法移位并异或(详看hash(key)的源码)。

假设有一种情况:如果数组长度是 16,也就是 15 (1111)

                             对象 A 的 hashCode :1000010001110001000001111000000 & 1111 = 0

                             对象 B 的 hashCode :0111011100111000101000010100000 & 1111 = 0

&运算这两个数, 你会发现结果都是 0。这样的散列结果太让人失望了。很明显不是一个好的散列算法。

但是如果我们将 hashCode 值右移 16 位,也就是取 int 类型的一半,刚好将该二进制数对半切开。并且使用位异或运算(如果两个数对应的位置相反,则结果为 1,反之为 0),这样的话,就能避免我们上面的情况的发生。
参考链接:https://hacpai.com/article/1514646296615

<2>:treeifyBin(tab, hash);

查看treeifyBin()源码:

/**
     * 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;
        // 如果数组等于null 或 数组长度小于 64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 重新散列,使得链表变短
            resize();
        // 如果hash冲突,且数组长度大于 64,则只能使用红黑树结构
        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);
        }
    }

// For treeifyBin 
    TreeNode replacementTreeNode(Node p, Node next) {
        // 返回一个新的红黑树
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

 <3>:resize();

重新散列函数resize()源代码:

    final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果旧容量大于0
        if (oldCap > 0) {
            // 如果旧容量大于等于2^30
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 阀值等于 Integer的最大值
                threshold = Integer.MAX_VALUE;
                // 返回旧数组,不扩充
                return oldTab;
            }// 如果旧容量*2 小于 最大容量   且   旧容量 大于等于 默认容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 新阀值 = 旧阀值*2
                newThr = oldThr << 1; // double threshold
        } // 如果旧阀值 大于 0
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 新容量 = 旧阀值
            newCap = oldThr;
        else {  // 如果容量是0,阀值也是0,认为这是一个新的数组,使用默认容量16 和 默认阀值12           
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果新的阀值是0,重新计算阀值
        if (newThr == 0) {
            // 新容量 * 负载因子(0.75)
            float ft = (float)newCap * loadFactor;
            // 如果新容量 小于 最大容量 且 阀值小于最大 
            // 则新阀值等于刚刚计算的阀值,否则新阀值为 int 最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        } 
        // 将新阀值 赋值 给当前对象的阀值。
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            // 创建一个Node 数组,容量是新数组的容量
            //(新容量 要么是 旧容量,要么是 旧容量*2,要么是16)
            Node[] newTab = (Node[])new Node[newCap];
        // 将新数组 赋值给 当前对象的数组属性
        table = newTab;
        // 如果旧数组 不是null
        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)
                        // 调用红黑树split()函数,将树的数据重新 散列 到数组中
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    // 如果不是树,next 节点也 不为空,则是链表,
                    //注意,这里将优化链表重新散列(jdk8 的改进)
                    else { 
                      // jdk8前,是并发操作,所以会出现环状链表,但jdk8 优化了此算法。
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {
                            next = e.next;
                            // 这里的判断需要引出一些东西:oldCap 假如是16,
                            // 那么二进制为 10000,扩容变成 100000,也就是32.
                            // 当旧的hash值 &运算 10000,结果是0的话,
                            // 那么hash值的右起第五位定是0,那么该于元素的下标位置也就不变。
                            if ((e.hash & oldCap) == 0) {
                                // 第一次进来时给链头赋值
                                if (loTail == null)
                                    loHead = e;
                                else
                                    // 在链尾巴赋值
                                    loTail.next = e;
                                // 重置该变量
                                loTail = e;
                            }
                            // 如果不是0,那么就是1,也就是说,如果原始容量是16,
                            // 那么该元素新的下标就是:原下标 + 16(10000b)
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 可将原链表拆成2组,优化查询。
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

三、Integer one=mapDemo.get("one")------生一个

查看源代码:

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * 

More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * *

A return value of {@code null} does not necessarily * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ public V get(Object key) { Node e; // key依旧被hash()函数处理过 return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; // 如果table不为空,且 table长度大于0,且下标:数组长度-1 与 key的hash,的值不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 数组的第一个节点的键和hash都等于传递进来的key和hash,则返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 如果数组的第一个节点的next属性不为空 if ((e = first.next) != null) { // 如果是树结构,则使用树获取值 if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); // 如果是链表结构,则使用while循环,获取值 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } // 返回null return null; }

 四、总结

这篇文章,除了介绍JDK8的hashmap的源码,其实也是在演示如何使用IntelliJ_IDEA来看我们想看的源码,很简单。

1、Ctrl + 鼠标点击方法/类 我们就可以看到对应的源码。

2、jdk8引入了树结构,来优化 链 过长所带来的性能低化的问题。

3、还有HashMap的初始容量总会是 2 的幂次方,因为HashMap的性能非常依赖这个 2 的幂次方。

 

容我再仔细想想总结,你们可以评论,我加上!

到此结束!

---------------------------------------------------------------------------不关注我“象话”吗?

如有疑惑,请评论留言。

如有错误,也请评论留言。

---------------------------------------------------------------------------

参考文章:

HashMap为什么初始容量为2的次幂:https://blog.csdn.net/ig_xdd/article/details/79065717

深入理解 hashcode 和 hash 算法:https://hacpai.com/article/1514646296615

深入理解 HashMap put 方法(JDK 8 逐行剖析):https://hacpai.com/article/1514726612565

你可能感兴趣的:(Java工具)