HashMap源码分析,基于1.8,对比1.7

一、HashMap概述

1、HashMap概述

HashMap以key-value形式存储数据,key允许为null,value也允许为null。

HashMap线程不安全,需要线程安全的HashMap可以使用ConcurrentHashMap。

HashMap底层是数组,又被称为hash桶,每个桶中存放的是链表,链表中的节点存储着集合中的元素。

JDK 1.8中当hash桶中链表长度达到 8 的时候会转成红黑树。

当HashMap中的元素数量达到threshold阈值的时候,会触发扩容,hash桶的长度翻倍。

HashMap中hash桶的长度一定是 2 ^ n。

HashMap中对于key落在哪个桶上,不仅仅会计算hashCode,还会经过扰动函数扰动使得key的分布更加均匀。

今天下面所有的分析都是基于JDK 1.8 的HashMap。

2、HashMap计算key在hash桶中落点的算法

HashMap的get方法会根据key的hashCode值计算出这个key的落点在hash桶的那个位置上。

通常这种计算hash值落点算法可以使用取余法,根据余数选择落点位置。

但是HashMap考虑到新能问题,利用了位运算的一个特性,将Hash桶的长度规定为 2 ^ n,然后利用tab[(tableSize – 1) & hash来得出不同hash值在hash桶中的落点位置。

3、hash()方法的扰动函数

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash()方法使用来获取key的hash值的,我们知道是哈希函数映射的比较均匀的话,碰撞的概率会小一些。

我们可以看到hash()方法中对key的hashCode值还进行了异或运算,下面来分析一下这样做的原因。

通过上面的分析我们知道计算key再hash桶中的落点采用了tab[(tableSize – 1) & hash算法,也就是取hash值的后几位来作为判断hash桶落点的依据。其实也就是对tableSize进行取余。

而hashCode的取值范围是Integer.MAX_VALUE,如果对tableSize进行取余的话,会忽略掉高位的数,只有低位的几个数会参与运算。

所以采用了异或运算,将 hashCode ^ (hashCode >>> 16) 将会使得hashCode的每一位都参与到取余运算中,这样就会使得哈希函数映射更加的均匀,降低hash碰撞的概率。

JDK 1.7中的扰动函数:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK 1.7中的扰动函数会对key进行四次扰动,可能考虑到过多次的扰动意义其实不大,所以JDK 1.8中将扰动函数修改为一次就好。

4、HashMap的构造方法

HashMap一共有四个构造方法:

public HashMap(){...}
public HashMap(int initialCapacity){...}
public HashMap(int initialCapacity, float loadFactor) {...}
public HashMap(Map<? extends K, ? extends V> m) {...}

其中前三个构造方法都是初始化一个空的HashMap,第四个构造方法会构造一个HashMap然后将参数中的 m 中的元素一一put到构造出来的HashMap中。

下面先来了解一下前三个构造方法:

要理解HashMap的构造方法需要先了解几个常量个成员变量

//hash桶的默认长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//HashMap中元素数量
transient int size;

//hash桶,用来存放链表和红黑树
transient Node<K,V>[] table;

//HashMap中元素的阈值,当size > threshold时,将发生扩容
int threshold;

//加载因子
final float loadFactor;

其中table被transient修饰,所以HashMap在序列化的过程中不会序列化size属性。

其中size被transient修饰,所以HashMap在序列化的过程中不会序列化size属性。

其中loadFactor被final修饰,所以一个HashMap对象的loadFactor一旦在对象初始化的时候被设置,就不能再进行修改。

第一个构造方法其实就是HashMap(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR)

第二个方法其实就是HashMap(initialCapacity, DEFAULT_LOAD_FACTOR)

第三个构造方法这两个变量都由HashMap对象创建的时候传入。

我们可以发现在初始化构造HashMap的时候并没有给table数组赋值,所以初始化HashMap的时候hash桶的size是 0,需要在第一次put操作的时候才会给hash桶赋值。这段赋值代码在resize方法中,下面讲到resize方法的时候再一起讲。

这里我们针对第三个构造方法拿出来讲一下:

public HashMap(int initialCapacity, float loadFactor) {
    //initialCapacity合法性校验
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //initialCapacity最大值校验
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //loadFactor合法性校验
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    //设置HaspMap对象的loafFactor 
    this.loadFactor = loadFactor;
    //设置HaspMap对象的threshold
    this.threshold = tableSizeFor(initialCapacity);
}

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

tableSizeFor这个方法使用了位运算的技巧,将返回第一个大于cap并且满足2 ^ n的数。

调用这个方法是为了将threshold设置为满足 2 ^ n 的数,之后在第一个put操作的时候会将这个threshold作为hash桶的size。

这样我们就成功的控制了hash桶的size满足 2 ^ n。

5、链表节点Node

下面我们来看一下链表中的节点Node,至于红黑树中的节点TreeNode这里不做介绍:

static class Node<K,V> implements Map.Entry<K,V> {
    //存放元素key的hash值
    final int hash;
    //存放元素的key
    final K key;
    //存放元素的value
    V value;
    //指向链表中下一个Node
    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;
    }

    //重写equals方法,只有当key和value都相等时返回true
    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;
    }
}

其中Node节点中存放一个hash字段来记录hash值而不是每次使用的时候再计算是因为每个Node的hash值都需要经过扰动函数的扰动,并且扩容的时候需要计算每个Node的hash值,所以出于空间换时间的想法,在Node节点中加入了hash字段。

6、扩容方法

当table没有初始化时进行put操作或者size > threshold的时候会进行扩容

final Node<K,V>[] resize() {
    //当前hash桶
    Node<K,V>[] oldTab = table;
    //当前hash桶的大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //当前hashMap的阈值
    int oldThr = threshold;
    //初始化新的hash桶的大小和hashMap阈值
    int newCap, newThr = 0;
    //如果当前hash桶的大小大于0
    if (oldCap > 0) {
        //如果当前hash桶的大小到达最大值,不再进行扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果当前hash桶的大小未达到最大值,将新的hash桶大小翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //如果旧的hash桶大小 >= 16,那么新的hashMap阈值也翻倍
            newThr = oldThr << 1; // double threshold
    }
    //如果当前hash桶大小为 0,但是阈值不为 0
    //那么表示初始化的HashMap对对象进行扩容
    else if (oldThr > 0) 
        //将当前hashMap的阈值赋值给新hash桶的大小
        //此时阈值和hash桶大小一致
        newCap = oldThr;
    //如果当前hash桶的大小和hashMap阈值都是0
    else {
        //设置新hash桶大小为默认值 16
        newCap = DEFAULT_INITIAL_CAPACITY;
        //设置新hashMap阈值为 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果当前hashMap的阈值为 0
    if (newThr == 0) {
        //根据当前hash桶的大小和加载因子计算新的阈值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //更新阈值
    threshold = newThr;
    //根据新hash桶的大小构建新的Node数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将新构建的Node数组赋值给table
    table = newTab;

    //省略部分是将当前hash桶中所有的节点转移到新的hash桶中
    ...

    return newTab;
}

上面这段代码分析了扩容过程中关于hash桶的大小以及hashMap阈值的变化,看完这段可以比较清晰的了解扩容时Node数组的变化。

二、HashMap扩容详细分析

其中关于扩容过程集合中的元素从原hash桶中向新hash桶的转移过程没有介绍,下面来介绍一下扩容时元素的转移过程。

1、JDK 1.7中HashMap扩容过程

所以这里先简单分析一下JDK 1.7中的扩容过程:

void resize(int newCapacity) {
    //当前hash桶
    Entry[] oldTable = table;
    //当前hash桶大小
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        //如果当前hash桶大小达到上限,将阈值设置为Integer.MAX_VALUE
        threshold = Integer.MAX_VALUE;
        return;
    }

    //根据传入的newCapacity创建新的hash桶数组
    Entry[] newTable = new Entry[newCapacity];
    //专业元素
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //将新构建的Node数组赋值给table
    table = newTable;
    //更新阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

//转移元素方法
void transfer(Entry[] newTable, boolean rehash) {
    //新hash桶大小
    int newCapacity = newTable.length;
    //遍历当前hash桶
    for (Entry<K,V> e : table) {
        //如果桶中元素不是空,进入循环
        while(null != e) {
            //使用next指针指向链表下一个节点
            //防止下一个节点丢失
            Entry<K,V> next = e.next;
            //根据hashseed的值来判断是否需要使用rehash来减少Hash碰撞
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //找出key需要存放在新hash桶中的位置
            int i = indexFor(e.hash, newCapacity);
            //将当前转移的元素插入到新hash桶指定位置链表表头
            e.next = newTable[i];
            newTable[i] = e;
            //转移当前转移元素在旧hash桶中的下一个元素
            e = next;
        }
    }
}

//使用 & 运算代替取余算法计算需要分配的has桶位置
static int indexFor(int h, int length) {
    return h & (length-1);
}

上面这一段是JDK 1.7中对HashMap扩容使用的算法,简单概括一下扩容流程:
– 计算新hash桶的大小和阈值
– 根据新hash桶的大小生成新的hash桶数组
– 对当前hash桶中的元素进行转移
– 遍历hash桶
– 遍历制定下标hash桶中的待转移节点
– 根据节点hash值算出在新hash桶中的下标
– 使用头插法将待转移的节点插入到新hash桶中的单链表上
– 将新hash桶以及新hash桶的大小以及阈值设置到当前HashMap对象

2、JDK 1.7中HashMap扩容可以优化的地方

下面讲一下JDK 1.7中HashMap扩容过程中可以优化的地方,也就是JDK 1.8中优化的地方。

头插法

下关于待转移元素插入新hash桶时采用了头插法,在之前的文章HashMap扩容死循环中就提到了,就是因为采用了头插法才导致死循环的。

因为hash桶中存放的是链表头节点,所以头插法时间复杂度是 O(1),而尾插发时间复杂度将达到 O(n),所以基于效率的考虑,这里采用了头插法。

头插法是JDK 1.7扩容时整体逻辑所决定的,所以不是说修改就能修改的。

待转移元素位置的选择

我们假设一个key的hash值是 x,当前hash桶的大小是 n,这个key在当前hash桶中的位置 i = x / n(其实HashMap采用的是 x & (n-1),这里为了好理解采用取余算法);

扩容的时候新hash桶的大小将变成 2n,那么这个key在新hash桶中的位置 j = x / 2n。

我们可以很轻易的得出 j = i 或者 j = i + n。

所以原hash桶中某个下标为 i 的链表中的节点在扩容后一定会落在新hash桶中下标为 i 或 i + n 的位置上。

那么如果 x & n == 0 表示这个key会落在新hash桶中下标为 i 的位置,如果 x & n != 0 表示这个key会落在新hash桶中下标为 i + n 的位置。

链表太长,查询效率会低

如果链表长度太长,那么HashMap的查询效率会降低,可以考虑树结构。

3、JDK 1.8 HashMap扩容过程

final Node<K,V>[] resize() {
    //当前hash桶
    Node<K,V>[] oldTab = table;
    //当前hash桶的大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //当前hashMap的阈值
    int oldThr = threshold;
    //初始化新的hash桶的大小和hashMap阈值
    int newCap, newThr = 0;
    //如果当前hash桶的大小大于0
    if (oldCap > 0) {
        //如果当前hash桶的大小到达最大值,不再进行扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果当前hash桶的大小未达到最大值,将新的hash桶大小翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //如果旧的hash桶大小 >= 16,那么新的hashMap阈值也翻倍
            newThr = oldThr << 1; // double threshold
    }
    //如果当前hash桶大小为 0,但是阈值不为 0
    //那么表示初始化的HashMap对对象进行扩容
    else if (oldThr > 0) 
        //将当前hashMap的阈值赋值给新hash桶的大小
        //此时阈值和hash桶大小一致
        newCap = oldThr;
    //如果当前hash桶的大小和hashMap阈值都是0
    else {
        //设置新hash桶大小为默认值 16
        newCap = DEFAULT_INITIAL_CAPACITY;
        //设置新hashMap阈值为 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果当前hashMap的阈值为 0
    if (newThr == 0) {
        //根据当前hash桶的大小和加载因子计算新的阈值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //更新阈值
    threshold = newThr;
    //根据新hash桶的大小构建新的Node数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将新构建的Node数组赋值给table
    table = newTab;

    if (oldTab != null) {
        //遍历当前hash桶
        for (int j = 0; j < oldCap; ++j) {
            //定义节点e用来指向待转移的节点
            Node<K,V> e;
            //如果当前下标为 j 的桶中没有元素,直接结束
            if ((e = oldTab[j]) != null) {
                //将当前下标为 j 的桶释放
                oldTab[j] = null;
                //如果当前桶的中只有一个节点
                //直接计算出该key在新hash桶中对应的位置
                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;
                    //定义高位链表的头尾节点
                    //其中高位链表在hash桶中的下标比低位节点大oldCap
                    Node<K,V> hiHead = null, hiTail = null;
                    //定义临时节点
                    Node<K,V> next;
                    do {
                        //使用next指针指向链表下一个节点
                        //防止下一个节点丢失
                        next = e.next;
                        //利用 & 运算得出key落在低位链表中
                        if ((e.hash & oldCap) == 0) {
                            //如果低位链表为空
                            //直接将待转移元素置为表头
                            if (loTail == null)
                                loHead = e;
                            //低位链表不为空
                            //采用尾插法将待转移元素插入到低位链表中
                            else
                                loTail.next = e;
                            //更新低位链表尾指针
                            loTail = e;
                        }
                        //利用 & 运算得出key落在高位链表中
                        else {
                            //执行逻辑和低位链表一致,不再赘述
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    //循环直到当前hash桶对应下标中没有元素为止
                    } while ((e = next) != null);
                    //如果低位链表不为空
                    //将低位链表赋值给低位hash桶中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //如果高位链表不为空
                    //将低位链表赋值给高位hash桶中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }

    return newTab;
}

上面这一段是JDK 1.8中对HashMap扩容使用的算法,简单概括一下扩容流程:
– 计算新hash桶的大小和阈值
– 根据新hash桶的大小生成新的hash桶数组,如果当前hash桶为空,构造一个长度默认的hash桶(JDK 1.7中在扩容前会先检查当前hash桶是否为空)
– 将新hash桶以及新hash桶的大小以及阈值设置到当前HashMap对象
– 对当前hash桶中的元素进行转移
– 遍历hash桶
– 遍历指定下标hash桶中的待转移节点
– 如果指定下标hash桶中待转移节点只有一个,直接计算在新hash桶中的落点并转移到新hash桶中
– 如果指定下标hash桶中存储的是树,按照树的结构来转移(暂不做介绍)
– 如果指定洗标hash桶中存的是链表
– 创建低位链表头尾指针和高位链表头尾指针
– 将待转移元素按照尾插法插入到低位链表和高位链表中
– 将低位hash桶和高位hash桶分别指向低位链表和高位链表
– 返回新的hash桶

4、JDK 1.7 和 JDK 1.8关于HashMap扩容的区别

  • JDK 1.7在扩容前会判断hash桶是否为空,如果为空会提前创建;JDK 1.8会在扩容时检查hash桶是否为空
  • JDK 1.7采用头插法将待转移元素插入到新hash桶链表中,多线程场景下会导致死循环;JDK 1.8采用尾插法,规避了死循环的情况
  • JDK 1.7扩容后链表中的元素会倒置;JDK 1.8扩容后链表中的元素不会倒置
  • 由于JDK 1.8对于链表长度达到 8 时会转成红黑树,所以扩容过程中考虑到了红黑树的情况
  • JDK 1.8在链表转移的时候引入了低位链表和高位链表进行转移,效率更高

虽然JDK 1.8中规避了HashMap扩容过程中遇到的死循环问题,但是HashMap仍然是线程不安全的,JDK 1.8只是对HashMap做了一些优化,但是多线程场景下还是不能使用HashMap的。

三、HashMap常用方法源码

下面再来分析一下HashMap中常用的方法。

下面部分文章基于JDK 1.8,就不再介绍JDK 1.7中的方法了。

1、get()方法

public V get(Object key) {
    //定义临时节点用于返回结果
    Node<K,V> e;
    //先调用hash(key)方法获得key的hash值
    //调用getNode方法找到Node节点
    //如果Node节点不存在返回null,存在的话返回Node节点的value
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//使用扰动函数使得hash值分布更加均匀
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//根据hash和key查询Node节点
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //tab[(n - 1) & hash]可以找到key对应在hash桶中的位置
    //如果hash桶不为空并且key对应hash桶的位置上存在节点,进入遍历
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {

        //如果第一个节点的hash值和key值和当前查询的key一致,直接返回第一个节点
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //第一个节点的hash值或者key值和查询的key不一致
        //并且存在下一个节点,执行下面方法
        if ((e = first.next) != null) {
            //如果当前节点在红黑树中
            if (first instanceof TreeNode)
                //调用getTreeNode方法获得红黑树中对应的节点(暂时不做介绍)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //如果当前节点不在红黑树中,也就是链表中,进入循环
            do {
                //如果循环中某个节点的hash值和key值一致,返回这个节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            //当不存在下一个节点的时候结束循环
            } while ((e = e.next) != null);
        }
    }
    //如果hash桶为空或者key对应hash桶的位置上不存在节点,返回null
    return null;
}

上面这一段是HashMap中get()方法的源码,简单概括一下扩容流程:
– 计算需要查询的key的hash值
– 判断hash桶是不是空以及该hash值对应在hash桶上是否存在节点,不存在直接返回null
– 对比头节点的hash值和key是否和需要查询的key一致,如果一致直接返回头节点
– 判断头节点在红黑树中还是链表中
– 如果在红黑树中,则在红黑树中查找该节点
– 如果在链表中,则遍历链表查询该节点

下面来分析几个小细节:

细节一、先比较hash再比较key

我们发现判断HashMap中节点是否是需要查询的key的时候使用的判断条件是

e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))

会先比较一下hash值是否一致再比较key是否相等,这么做是也是为了查询效率。

因为通常key都会重写equals方法,通过equals方法比较的效率明显是比hash值比较要慢的。

所以再equals判断之前先比较一次hash值可以提前规避很多不符合的节点,效率上会高一些。

细节二、先比较头节点,再遍历链表(红黑树)

我们发现当取到key对应hash桶中的节点的时候,会先比较一下头节点是否是我们需要查询的节点,如果不符,再去遍历链表(红黑树)。

因为在hash算法足够分散的情况下,很多hash桶的每一个桶中都只会存在一个节点,所以先比较第一个节点再去遍历也能带来效率上的提升。

2、put()方法

public V put(K key, V value) {
    //先调用hash(key)方法获得key的hash值
    //调用putVal方法插入数据,并返回结果
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //定义临时变量
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果hash桶为空
    if ((tab = table) == null || (n = tab.length) == 0)
        //扩容并将扩容后hash桶的大小赋值给n
        n = (tab = resize()).length;
    //如果key对应hash桶的位置上没有节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        //将put进来的key,value生成一个Node节点
        //并将该节点赋值给hash桶指定下标上
        tab[i] = newNode(hash, key, value, null);
    //如果key对应hash桶的位置上有节点
    else {
        //定义临时变量
        Node<K,V> e; K k;
        //如果头节点的hash值和key和需要put的key一致
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //将头节点赋值给临时变量p,用于下面返回
            e = p;
        //如果头节点的hash值和key和需要put的key不一致
        //并且头节点在红黑树中
        else if (p instanceof TreeNode)
            //调用红黑树的putTreeVal方法将key,value设置到红黑树中
            //并且如果红黑树中存在该key,将对应的value赋值给e
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //如果头节点的hash值和key和需要put的key不一致
        //并且头节点在链表中
        else {
            //累计便利次数,用于在下面链表长度达到 8 的时候转成红黑树
            for (int binCount = 0; ; ++binCount) {
                //如果链表中不存在下一个节点
                if ((e = p.next) == null) {
                    //将put进来的key,value生成一个Node节点
                    //并将该节点按照尾插法插入到链表尾部
                    p.next = newNode(hash, key, value, null);
                    //如果便利次数达到 7 次,那么插入新节点后链表长度将达到 8
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        //将链表转成红黑树
                        treeifyBin(tab, hash);
                    //跳出循环
                    break;
                }
                //如果链表中节点的hash值和key和需要put的key一致
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    //跳出循环
                    break;
                //链表中对应节点赋值给临时节点p
                p = e;
            }
        }
        //如果e != null,表示链表(红黑树)中存在相同的key
        if (e != null) { // existing mapping for key
            //记录旧值,用于返回
            V oldValue = e.value;
            //如果onlyIfAbsent == false或者旧值为空(这里下面再讲)
            if (!onlyIfAbsent || oldValue == null)
                //使用调用put方法传递进来的value覆盖链表中节点中的value
                e.value = value;
            //这个方法是为LinkedHashMap留的后路
            afterNodeAccess(e);
            //返回旧值
            return oldValue;
        }
    }
    //能走到这一步,在上面if (e != null)判断中 e == null
    //表示新增了元素而不是替换了元素
    //modCount记录了HashMap在节点数量上变化的次数,在这里加一
    ++modCount;
    //当前HashMap中元素加一,并且和阈值比较
    if (++size > threshold)
        //如果当前HashMap中元素数量大于阈值,进行扩容
        resize();
    //这个方法是为LinkedHashMap留的后路
    afterNodeInsertion(evict);
    //由于是新增节点,没有覆盖旧节点的值,返回null
    return null;
}

上面这一段是HashMap中put()方法的源码,简单概括一下扩容流程:
– 计算需要put的key的hash值
– 判断hash桶是不是空,为空先进行扩容
– 判断该key值对应在hash桶上是否存在节点,不存在则直接在hash桶中创建节点
– 对比头节点的hash值和key是否和需要查询的key一致,如果一致直接覆盖头节点
– 判断头节点在红黑树中还是链表中
– 如果在红黑树中,则在红黑树中查找该节点,如果在链表中,则遍历链表查询该节点
– 如果在链表(红黑树)中存在节点的hash值和key和需要put的key一致,进行覆盖操作
– 如果不存在,创建新的节点,添加到链表(红黑树)中
– 如果当前HashMap中元素数量超过阈值,进行扩容
– 如果put()方法覆盖了某个节点,则返回这个节点的value,否则返回null

下面我们来分析一下这段代码:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

public V putIfAbsent(K key, V value) {
    return putVal(hash(key), key, value, true, true);
}

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    ...  
     if (!onlyIfAbsent || oldValue == null)
                //使用调用put方法传递进来的value覆盖链表中节点中的value
                e.value = value
    ...
}

通过这段代码我们发现,如果put方法的key在HashMap中存在,put方法一定会使用新的值去覆盖旧的值;如果putVal方法的key在HashMap中存在,只有在旧值为空的情况下putVal方法才会使用新的值去覆盖旧的值。

3、clear()方法

public void clear() {
    //定义临时变量
    Node<K,V>[] tab;
    //将HashMap节点变化次数加一(如果当前hash桶是空也会增加)
    modCount++;
    //如果当前hash桶不是空,则遍历hash桶,hash桶中所有的桶都置空
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

clear()方法很简单,就不做介绍了。

4、总结

到这里,关于HashMap的get()、put()、clear()方法的源码也都看完了。

这篇文章把HashMap主要的源码和实现方式介绍了一下,关于红黑树方面的内容都略过了,没有做过多介绍,以后有时间再分享一下红黑树相关的源码。

喜欢这篇文章的朋友,欢迎扫描下图关注公众号lebronchen,第一时间收到更新内容。
《HashMap源码分析,基于1.8,对比1.7》

    原文作者:Lebron_Chen
    原文地址: https://blog.csdn.net/leon_cx/article/details/81947991
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞