java之HashMap源码(一)

84 阅读5分钟

本片重点

  1. HashMap存储结构
  2. HashMap的put和get方法
  3. HashMap扩容机制

HashMap存储结构

在jdk1.8以前,HashMap的存储结构是数组+链表,jdk1.8之后,引入了红黑树,当链表长度等于8时,链表就会转为红黑树,网上很多人说链表长度大于8就会转为红黑树,其实这是不够准确的,至于为什么,待会往下看源码就知道。

image.png 数组的特点:查询效率高,插入,删除效率低。

链表的特点:查询效率低,插入删除效率高。

红黑树的特点:查询效率高,插入删除效率低。

HashMap之put方法

接下来,我们来看看HashMap的put方法的源码。

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

在HashMap的put方法里面,调用putVal方法,这个方法传了五个参数,其中有一个是hash(key),调用了hash方法,传了一个key进去,这个方法是干什么的呢?顾名思义,就是计算key的hash值。我们进去这个方法看看怎么处理的

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

可以看到,当key==null时,返回0,当key不等null时,将key的hash值与其的hash值无符号右移16位进行异或运算得到的结果返回,最终得到了key的hash值。

接下来,我们进入到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;
            }
        }
        //新的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;
}

这里有几个步骤:

1.初始化数组:

这个方法里面看到第一个判断if ((tab = table) == null || (n = tab.length) == 0),tabel就是hashmap数据结果中的数组。如果数组为空或者数组长度为0时,调用resize()方法(后面会讲),对数组进行初始化。

2.计算key在数组中的位置:

第二个判断if ((p = tab[i = (n - 1) & hash]) == null),i = (n - 1) & hash 就是将数组的大小与key的hash值进行与运算(&),得到key所在数组的位置。当key所在的位置为空时,直接插入数据并判断是否需要扩容。

注意:这里可能有人会有疑惑,为什么使用与运算,首选我们要明白,与运算得到的结果都会小于等于左右两边最小的值,也就是说,这里的i不管结果是啥,都是小于等于(n - 1) 或者 hash,而hash是远大于n-1的,所以就保证了i小于等于n-1。

3.key比较

if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))。

当key所在的位置不为空时,首先会先判断新插入的key和原来的key的hash值是否相等,如果相等,也就是发生hash冲突时,会进行equal比较,如果相等则将value覆盖。如果不相等,则判断节点是否是红黑树

else if (p instanceof TreeNode)

如果是红黑树,则在红黑树插入数据,否则在链表中插入数据

4.红黑树中插入数据

if (binCount >= TREEIFY_THRESHOLD - 1) // TREEIFY_THRESHOLD = 8
    treeifyBin(tab, hash);

当链表长度大于等于7时,执行treeifyBin(tab, hash)方法。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //MIN_TREEIFY_CAPACITY = 64
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> 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);
    }
}

treeifyBin方法有个判断,(n = tab.length) < MIN_TREEIFY_CAPACITY,MIN_TREEIFY_CAPACITY为常量64,如果数组大小小于64,执行resize()进行扩容,而不是转为红黑树,所以HashMap链表转为红黑树的前提条件是:“链表长度大于等于7并且数组大小大于等于64”。只有符合条件了,链表才会执行do-while循环,将链表转为红黑树。replacementTreeNode(e, null)方法将链表节点转为树节点,treeify(tab)生成红黑树。红黑树的插入比较复杂,这里不多讲。

5.链表中插入数据

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

首选我们要明白,链表有头指针(heade),节点(node),尾指针(tail),每一个节点有一个next指向下一个节点。链表插入操作是遍历循环链表,(e = p.next) == null,判断当前节点的next是否为null,如果为null,插入一个新节点(node),插入节点后,判断链表个数是否大于等于数组长度-1,如果是,则转为红黑树。也就是说当链表的长度大于等于数组长度-1时,就会转为红黑树。新增一个节点之后需要判断是否需要扩容

当前节点的next不为null时,判断key的值是否相等,如果相等,结束循环,执行后面的代码,将新的value覆盖原来的value

//新的value覆盖原来的值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;  //覆盖原来的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //覆盖值之后,需要对链表的节点挪位置,这里不讲,感兴趣可以自己研究
            afterNodeAccess(e); 
            return oldValue;
        }

put方法在新增数据时需要根据threshold判断是否需要扩容

if (++size > threshold)
        resize();

put方法操作步骤总结:

1.计算出key的hash值

2.如果是第一次插入,需要初始化数组(懒加载),默认大小为16

3.根据key的hash值和数组的大小计算出key在数组中的位置

4.如果当前位置为空,则在当前位置中插入一条数据,然后根据threshold判断数组是否需要扩容

5.如果当前位置不为空,即发生hash冲突,判断key是否相等,如果相等则覆盖原来的值,不相等则判断是否存在红黑树,存在则在红黑树中插入数据,不存在则在链表中插入数据,如果链表的长度大于等于数组长度-1,并且数组大小大于等于64时,则链表转为红黑树。

**总结:**

这篇文章就讲到这里,主要讲了HashMap的结构,put方法源码,下一篇讲HashMap的get方法。