按部就班分析源码 - HashMap put 方法的执行流程(jdk 1.7 )

开篇

本篇博客主要分析 jdk1.7 中的 HashMap 的 put() 方法。接下来是几点说明:算法

  • 经过画流程图的方式分析方法的执行流程,不会细致到具体每一个方法,好比 hash 算法。
  • 不讲 HashMap 的相关概念以及使用方法, 可能只会提一下。
  • 文章贴出的代码注释很重要。
  • 具体细节以后会慢慢补充, 按部就班。

进入正题

预备知识

jdk1.7 中 HashMap 采用的是数组 + 链表的数据结构(ps:若是数组和链表是什么东西,不建议往下看),以下图所示:数组

jdk1.7 中的HashMap的数据结构

当 put 一个元素时, 会先计算出元素在数组中的所在位置, 若是那个位置已经有元素存在, 这时就发生了哈希冲突(或叫哈希碰撞)。至于发生哈希冲突后, 稍后详解。安全

注意: 这张图很重要, 下面会反复提到数据结构

HashMap 源码中的数组和链表如何定义的?学习

链表中的节点定义为 Entry<K,V>, 详情能够看下面的代码段(最好结合上面的图)。this

// 链表中的节点
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;  // 存放 键
    V value;   // 存放 值
    Entry<K,V> next;// 指向链表中的下一个节点
    int hash;// 节点对应的hash值

    // 此处省略 set/get 等方法
}

// 数组 : 默认为空数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

这里插一句线程

好比上面的最后一行代码,里面有一个 transient 关键字, 它的的用法和含义咱们暂时不用了解,着急的话请自行学习,后续的文章可能也会补充,本文咱们只讲流程,它并不会妨碍咱们。 说这句话是由于我以前看的时候,就想把每一处都了解清楚,从而致使耗费了不少时间,最后发现脑子里一团浆糊, 这里再次强调 按部就班code

由数组和链表的定义,咱们能得出什么?blog

数组中能够存放一个 Entry,也能够存放一个 Entry 链。(结合上面的图)索引

HashMap 中的哈希算法?

把 put 方法传入的 key,通过算法计算,获得一个 int 类型的数字并返回。

key 为 null 的元素放在哪?

数组(table[])中索引为 0 的位置, (再次结合上面的图)

比较重要的几个变量

这里说几个 put 方法中用到的几个变量。

// 一个大小为0 的空数组
static final Entry<?,?>[] EMPTY_TABLE = {};

// 存放元素的数组,默认指向上面的空数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

put 方法的源代码(无注释)

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

put 方法的源代码(有注释)

public V put(K key, V value) {
    // 若是容器为空, 就初始化容器
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    
    // 若是 key 为null,放置在第 0 个 桶的位置
    if (key == null)
        return putForNullKey(value);
        
    // 根据 key 计算 hash值
    int hash = hash(key);
    
    // 根据 hash 值计算出应该存放到数组中的哪一个位置(暂时命名为位置 A)
    int i = indexFor(hash, table.length);
    
    // 判断位置 A 是否已经被占据,若是是,就遍历该位置上的链表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 若是链表中知足如下条件(命名为条件1)的 Entry ,就覆盖掉它
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;// 略过,不作分析
    
    // 若是位置 A上是空的
    // 或者位置 A 中的链表不存在知足条件1的 Entry,就建立一个Entry 放置在位置A, 充当链表表头。
    addEntry(hash, key, value, i);
    
    return null;
}

流程图

这里咱们就看下完整的流程图是什么样的。

jdk1.7 HashMap 的 put 方法执行流程图

总结

上面不少细节都没讲, 好比:hash 算法是怎么实现的,为何? HashMap 的扩容机制,扩容因子为何是 0.75,为何是两倍扩容,甚至其余更难的点,HashMap 为何是非线程安全的等等。 上面这些问题是我学习 HashMap 源码时遇到的问题,基本看到一个方法就想点进去看细节,最后发现没头没尾的。而本文只是先描述下 put 方法的大体执行流程,以后会逐个击破上面提到的问题, 按部就班。

相关文章
相关标签/搜索