HashMap put值时第一次put和第二次put以及hash冲突时源码步骤解析

本文详细解析了HashMap在Java中第一次及第二次存值的具体流程,包括hash计算、节点存放及冲突处理等关键步骤。


HashMap源码还得经常看,了解过源码之后过一段时间还是会忘记,正好有时间再来回顾一下向HashMap put值时所走的流程。如有错误,欢迎各位大佬们指正。

第一次put值

当new一个HashMap的时候 hashMap的数组长度为0,第一次put值的时候才会给hashMap一个初始化的数组长度。

一、当第一次向HashMap put值时需要走以下代码

1、put( )

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
1.1、hash( )
static final int hash(Object key) {
    int h;
    //判断key是否为空,如果为空则返回0否则返回key.hashCode()异或上key.hashCode()向右位移16位的值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2、putVal( )

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断tab是否为空如果为空则给数组分配一个长度走resize()方法
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
	//判断hash桶是否已被其他值占用,如果占用则走else,否则直接将key value放入node节点    
	if ((p = tab[i = (n - 1) & hash]) == null)
        //new一个新的node节点,将key和value放进去,因为是第一次put所以next指针为null。
        tab[i] = newNode(hash, key, value, null);
    else{
        /***************第一次put值时不走的代码已省略掉***************/  
    }                                           
    //经过结构修改的结构修改的次数+1,modCount是为hashmap修改的次数计数
    ++modCount;
    //如果size+1大于threshold(数组长度*负载因子的值可称之为阈值)则需要给数组扩容
    if (++size > threshold)
        resize();
    //这个方法是空的,在LinkedHashMap中,使用LRU算法,该方法有内容,实际是为了扩展其他
    afterNodeInsertion(evict);
    return null;
}
2.1 resize( )
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //如果老数组是否为null,是则长度为0否则为oldTab.length
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //将数组长度与负载因子的积(阈值)赋给oldThr
    int oldThr = threshold;
    int newCap, newThr = 0;
    //第一次向hashMap put值oldCap肯定为0,所以此判断不走
    if (oldCap > 0) {
        ...
    }
    //oldThr也不大于0,走else
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //将DEFAULT_INITIAL_CAPACITY(1向左位移4=16)的值赋给newCap
        newCap = DEFAULT_INITIAL_CAPACITY;
        //将DEFAULT_LOAD_FACTOR(负载因子)*DEFAULT_INITIAL_CAPACITY(16)的积赋给newThr
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //已经给newThr赋值,不走此代码
    if (newThr == 0) {
        ...
    }
    //将newThr的值赋给阈值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //初始化newTab,给newTab一个长度
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将空数组newTab赋给table
    table = newTab;
    //oldTab为空不走此代码
    if (oldTab != null) {
        ...
    }
    return newTab;
}
2.2 newNode( )
// 创建一个node节点
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

至此,第一次put数据流程走完,HashMap的Node节点长度为16.

二、第二次put值

1、put

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
1.1、hash()
static final int hash(Object key) {
    int h;
    //判断key是否为空,如果为空则返回0否则返回key.hashCode()异或上key.hashCode()向右位移16位的值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2、putVal

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //由于第一次put时已将数据放入table中,此时table!=null所以不走
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //判断数组长度与上新key的hashcode是否已被占用,如果占用则hash冲突,走else,否则直接存值
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
         /***************此时else还不走,当出现hash碰撞时,才会走else代码***************/  
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
2.1 newNode( )
// 创建一个node节点
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

至此第二次put数据流程走完,HashMap中经过两次put已将数据存放至hashmap中,虽然看似两次put所走的代码一样(除了第一次put时resize()了),但是只是没有出现hash冲突的情况,如果两次put的key存在hash冲突,则会将第二次的数据和第一次的数据放入同一个链表中。

三、hash冲突时

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //当出现hash冲突时tab[i = (n - 1) & hash]!=null,走else
        else {
            Node<K,V> e; K k;
            //老key的hash值==新key的hash并且老key==新key的话会将新value覆盖老value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果key不相同,那么判断数据结构是否为红黑树,如果为红黑树则直接将新节点插入树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果还是链表的话,走for循环,循环的对象是p的next节点不为null或者p的后续节点有key相同,如果key相同,那么找到这个节点,用新值替换。否则,往链表的最后一点节点添加数据,因为遍历找链表,使用binCount来记录遍历个数,当遍历的个数达到门限值(默认是8)达到8个那么就要发生树化改造
            else {
                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;
                }
            }
            //正如上边所述key相同,则将value替换老的value
            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;
    }

四、总结

用大白话来说:

  • 第一次put值时,先判断节点是否为空,如果为空则resize一个长度,并将key,value放入节点中。
  • 第二次put值时,判断是否存在hash冲突,如果未冲突则直接将key,value放入节点中,如果出现hash冲突,则看key是否相同,相同则直接将新value替换旧value,不同则判断数据结构是否为树形结构,是则将节点插入树中,不是则为链表,便遍历链表知道next节点为null时将节点放入链表中,如果此时发现再插入此节点链表长度就大于树化负载因子的话,那么就对链表进行树化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Silence-wen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值