循序渐进分析源码 - HashMap put 方法的执行流程(jdk 1.7 )

开篇

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

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

进入正题

预备知识

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

循序渐进分析源码 - HashMap put 方法的执行流程(jdk 1.7 )_第1张图片

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

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

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

链表中的节点定义为 Entry, 详情可以看下面的代码段(最好结合上面的图)。

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

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

// 数组 : 默认为空数组
transient Entry[] table = (Entry[]) EMPTY_TABLE;

这里插一句

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

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

数组中可以存放一个 Entry,也可以存放一个 Entry 链。(结合上面的图)

HashMap 中的哈希算法?

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

key 为 null 的元素放在哪?

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

比较重要的几个变量

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

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

// 存放元素的数组,默认指向上面的空数组
transient Entry[] table = (Entry[]) 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 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 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;
}

流程图

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

循序渐进分析源码 - HashMap put 方法的执行流程(jdk 1.7 )_第2张图片

总结

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

转载于:https://my.oschina.net/u/3984985/blog/3098587

你可能感兴趣的:(循序渐进分析源码 - HashMap put 方法的执行流程(jdk 1.7 ))