Java HashMap的put过程

本文详细解析了Java HashMap的put方法实现,包括计算hash值、解决hash冲突及链表与红黑树转换策略,深入理解HashMap的工作原理。

1. 通过源码来分析HashMap的put过程

Object key = new Object();
Object value = new Object();
Map<Object, Object> map = new HashMap<Object, Object>();
map.put(key, value);

当我们执行上面这段代码时,HashMap的底层是怎么的一个实现过程呢,下面我们就来一一揭晓。
首先程序会执行HashMap里的put方法就是下面的这个代码啦。

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

从这个put方法的代码可以看出首先调用hash方法,这个方法时干嘛的呢?实际就是计算这个key的hash值,hash方法的代码如下,经过这个操作之后就获取到了key的hash值。

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

然后就调用了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;
    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);
    else {
        Node<K,V> e; K k;
        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) {
                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;
}

首先这里先判断一下这个存放元素的数组是不是null(我这里分析的是JDK1.8的代码,在jdk1.8中new HashMap的时候不会先创建这个数组,而是在第一次添加元素的时候创建)。

if ((tab = table) == null || (n = tab.length) == 0)
	n = (tab = resize()).length;

就是这个if 判断,如果数组是空,就调用 resize方法去new出这个数组并且大小为16(默认初始大小)然后就去计算当前我们添加的这个元素在数组中的索引,代码如下。

if ((p = tab[i = (n - 1) & hash]) == null)
	tab[i] = newNode(hash, key, value, null);

代码中的 i就是当前的这个元素应该在数组中的索引(p = tab[i = (n - 1) & hash]) == null,p表示当前的这个位置储存的元素。说明现在已经出现了hash冲突,下面就是解决hash冲突的方法。

2. 解决Hash冲突

  1. 如果 p == null就表示当前这个位置没有存储任何元素,就把我们当前新添加的 生产一个新的 Node 元素,并把这个新的Node元素存放在数组中 i 的位置。 newNode方法如下。
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
       return new Node<>(hash, key, value, next);
    }
    
  2. 否则(也就是 p != null)就是数组i的位置存有元素,就进行下面的操作。
    Node<K,V> e; K k;
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    
    首先比较p.hash == hash && ((k = p.key) == key数组i位置的元素p的hash等于我们新添加的元素key的hash(这个p是一个Node类型的,p的hash实际就是p的key的hash)并且这个元素p的key==我们新添加元素的key或者key != null && key.equals(k)))就表明我们添加的元素的key就是数组i位置p元素的key。后面就会更新这个p元素的value为我们新添加元素的value。 p.hash == hash && ((k = p.key) == key这个判断是为了比较当key是基本数据类型的时使用的。key != null && key.equals(k)))是为了当key为引用数据类型时使用的。
  3. 如果 元素p既不等于null,并且p的key也不是我们新添加的元素的key,这时就就需要进行下面的操作啦,就要把我们新添加的元素添加到数组i位置下组成的链表里
    for (int binCount = 0; ; ++binCount) {
       if ((e = p.next) == null) { // 如果p的后一个是null就把我们新添加的元素添加到这个链表的末尾
           p.next = newNode(hash, key, value, null);
           if (binCount >= TREEIFY_THRESHOLD - 1) // 判断这个链表的长度是不是大于阈值,大于就把链表转成红黑树
               treeifyBin(tab, hash);
           break;
       }
       //执行和上面解决hash冲突的第二步一样的判断和操作。
       if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
           break; //说明这个链表中存在元素的key和我们新添加的key是相等的,
       p = e;
    }
    
  4. 最后判断 e 是不是null,如果不是说明这个map中存在我们新添加的key,然后就执行下面的程序判断是不是要覆盖掉原来的value值。
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent 表示当原来的值为null的时候才替换我们添加的值
            e.value = value;
        // 执行后序操作,如果使用的是LinkedHashMap的话会去维护链表的结构。LinkedHashMap继承了HashMap
        afterNodeAccess(e); 
        return oldValue;
    }
    

最后执行下面的程序,表示我们新添加的元素,在map中没有相同的key,正常的添加到了map中,之后执行一些后序的操作

++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;

3.最后总结

Object key = new Object();
Object value = new Object();
Map<Object, Object> map = new HashMap<Object, Object>();
我们调用 map.put(key, value)方法时

  1. 计算key的hash值 ,hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  2. 通过一定的方法计算 key 在数组中的索引 i ,i = (n - 1) & hash]
  3. 判断数组中索引为i的位置是否已经有值,if ((p = tab[i = (n - 1) & hash]) == null)
    • 如果没有值,就 创建一个新的 Node,并且把这个新的Node存到这个数组索引为i的位置,tab[i] = newNode(hash, key, value, null);
    • 如果有值,就在当前元素组成的链表上搜索是否存在已经存在的Node的key我们新添加的key相同的。
      • 如果在当前的这个链表上存在Node的key和我们新添加的key相同,就把我们新添加的value覆盖掉这个Node的value。(实际上最后执行putVal方法时有个参数onlyIfAbsent可以控制是否覆盖)
      • 如果在当前的这个链表上没有Node的key和我们新添加的key相同,就把我们新添加的key和value组成一个新的Node添加到这个链表的最后面 (JDK1.7之前都是插在这个链表的最前面就是数组i的位置,这样就会存在线程安全的问题容易发生链表的环形结构。JDK1.8之后插到这个链表的最后面,这样也不是线程安全的,因为在1.8中的put主函数中如下,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中,造成先插入的数据被覆盖掉
        if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值