Java中HashMap源码分析

2019-02-09

1 HashMap的定义

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    // 默认的容量大小,该大小必须是2的幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 最大容量,2的30次方(若传入的容量过大,将被最大值替换)
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 加载因子:HashMap在其容量自动增加前可达到多满的一种尺度
    // a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
    // b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 一个桶的树化阈值
    // 当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
    // 这个值必须为 8,要不然频繁转换效率也不高
    static final int TREEIFY_THRESHOLD = 8;

    // 一个树的链表还原阈值
    // 当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
    // 这个值应该比上面那个小,至少为 6,避免频繁转换
    static final int UNTREEIFY_THRESHOLD = 6;

    // 哈希表的最小树形化容量
    // 当哈希表中的容量大于这个值时,表中的桶才能进行树形化
    // 否则桶内元素太多时会扩容,而不是树形化
    // 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
    static final int MIN_TREEIFY_CAPACITY = 64;
}

HashMap继承AbstractMap类,实现了Map、Cloneable和Serializable接口。

2 HashMap的构造方法

2.1 HashMap()

该构造函数是默认构造函数,意在构造一个具有:默认初始容量 (16) 和 默认负载因子(0.75) 的空 HashMap

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

2.2 HashMap(int initialCapacity)

该构造函数意在构造一个指定初始容量和默认负载因子 (0.75) 的空 HashMap

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

2.3 HashMap(int initialCapacity, float loadFactor)

指定“容量大小”和“加载因子”的构造函数。

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量不能小于 0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 与最大的容量对比,取两者中最小
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 负载因子不能小于 0,也不能为空
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // threshold是HashMap判断size是否需要扩容的阈值
    // tableSizeFor方法保证函数返回值是大于等于给定参数initialCapacity最小的2的幂次方的数值。
    this.threshold = tableSizeFor(initialCapacity);
}

2.4 HashMap(Map<? extends K, ? extends V> m)

包含“子Map”的构造函数。

/**
 * Constructs a new <tt>HashMap</tt> with the same mappings as the
 * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
 * default load factor (0.75) and an initial capacity sufficient to
 * hold the mappings in the specified <tt>Map</tt>.
 *
 * @param   m the map whose mappings are to be placed in this map
 * @throws  NullPointerException if the specified map is null
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

3 HashCode()

public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

4 HashMap保存数据的过程

  1. 首先,判断key是否为null;
    • 若为null,则直接调用putForNullKey方法;
    • 若不为空,则先计算key的hash值;
  2. 然后根据hash值搜索在table数组中的索引位置;
  3. 如果table数组在该位置处有元素,则查找是否存在相同的key,若存在则覆盖原来key的value;
  4. 否则将该元素保存在链头(最先保存的元素放在链尾)。此外,若table在该处没有元素,则直接保存。

5 HashMap读取数据的过程

HashMap只需通过key的hash值定位到table数组的某个特定的桶,然后查找并返回该key对应的value即可。

调用HashMap的get(Object key)方法后,若返回值是 NULL,则存在如下两种可能:

  • 该 key 对应的值就是 null;
  • HashMap 中不存在该 key。

6 HashMap 的底层数组长度为何总是2的n次方?

  • 不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,空间利用率较高,查询速度也较快;
  • h&(length - 1) 就相当于对length取模,而且在速度、效率上比直接取模要快得多,即二者是等价不等效的,这是HashMap在速度和效率上的一个优化。

参考文献

源码分析之 HashMap
Java HashMap工作原理及实现
Java:手把手带你源码分析 HashMap 1.7
Map 综述(一):彻头彻尾理解 HashMap
Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例
Java 集合深入理解(16):HashMap 主要特点和关键方法源码解读