构造函数

构造函数

HashMap 一共提供了四个构造函数,其中 默认无参的构造函数 和 参数为 Map 的构造函数 为 Java Collection Framework 规范的推荐实现,其余两个构造函数则是 HashMap 专门提供的。

HashMap()

该构造函数意在构造一个具有> 默认初始容量 (16) 和 默认负载因子(0.75) 的空 HashMap,是 Java Collection Framework 规范推荐提供的,其源码如下:

/**
* Constructs an empty HashMap with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {

    //负载因子:用于衡量的是一个哈希表的空间的使用程度
    this.loadFactor = DEFAULT_LOAD_FACTOR;

    //HashMap进行扩容的阈值,它的值等于 HashMap 的容量乘以负载因子
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);

    // HashMap的底层实现仍是数组,只是数组的每一项都是一条链
    table = new Entry[DEFAULT_INITIAL_CAPACITY];

    init();
}

在构造函数中都会涉及初始容量负载因子,这两个参数是影响 HashMap 性能的重要参数。其中,容量表示哈希表中桶的数量 (table 数组的大小),初始容量是创建哈希表时桶的数量;负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个哈希表的空间的使用程度,负载因子越大表示哈希表的装填程度越高,反之愈小。对于使用拉链法的哈希表来说,查找一个元素的平均时间是 O(1+a),a 指的是链的长度,是一个常数。特别地,若负载因子越大,那么对空间的利用更充分,但查找效率的也就越低;若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重。系统默认负载因子为 0.75,这是时间和空间成本上一种折衷,一般情况下我们是无需修改的。

此外,每次新建一个 HashMap 时,都会初始化一个 Entry 类型的 table 数组,其中 Entry 类型的定义如下:

static class Entry<K,V> implements Map.Entry<K,V> {

    final K key;     // 键值对的键
    V value;        // 键值对的值
    Entry<K,V> next;    // 下一个节点
    final int hash;     // hash(key.hashCode())方法的返回值

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {     // Entry 的构造函数
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    ......

}

其中,Entry 为 HashMap 的内部类,实现了 Map.Entry 接口,其包含了键 key、值 value、下一个节点 next,以及 hash 值四个属性。事实上,Entry 是构成哈希表的基石,是哈希表所存储的元素的具体形式。

HashMap(int initialCapacity)

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

// Constructs an empty HashMap with the specified initial capacity and the default load factor (0.75)
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);  // 直接调用上述构造函数
}

HashMap(int initialCapacity, float loadFactor)

该构造函数意在构造一个 指定初始容量 和 指定负载因子的空 HashMap,其源码如下:

/**
* Constructs an empty HashMap with the specified initial capacity and load factor.
*/
public HashMap(int initialCapacity, float loadFactor) {
    //初始容量不能小于 0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

    //初始容量不能超过 2^30
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    //负载因子不能小于 0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                            loadFactor);

    // HashMap 的容量必须是2的幂次方,超过 initialCapacity 的最小 2^n
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    //负载因子
    this.loadFactor = loadFactor;

    //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行自动扩容操作
    threshold = (int)(capacity * loadFactor);

    // HashMap的底层实现仍是数组,只是数组的每一项都是一条链
    table = new Entry[capacity];
    init();
}

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

该构造函数意在构造一个与指定 Map 具有相同映射的 HashMap,其 初始容量不小于 16 (具体依赖于指定 Map 的大小),负载因子是 0.75,是 Java Collection Framework 规范推荐提供的,其源码如下:

// Constructs a new HashMap with the same mappings as the specified Map.
// The HashMap is created with default load factor (0.75) and an initial capacity
// sufficient to hold the mappings in the specified Map.
public HashMap(Map<? extends K, ? extends V> m) {

    // 初始容量不小于 16
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                    DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    putAllForCreate(m);
}

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

我们知道,HashMap 的底层数组长度总是 2 的 n 次方,原因是 HashMap 在其构造函数 HashMap(int initialCapacity, float loadFactor) 中作了特别的处理,如下面的代码所示。当底层数组的 length 为 2 的 n 次方时, h&(length - 1) 就相当于对 length 取模,其效率要比直接取模高得多,这是 HashMap 在效率上的一个优化。

// HashMap 的容量必须是2的幂次方,超过 initialCapacity 的最小 2^n
int capacity = 1;
while (capacity < initialCapacity)
    capacity <<= 1;

在上文已经提到过,HashMap 中的数据结构是一个数组链表,我们希望的是元素存放的越均匀越好。最理想的效果是,Entry 数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过 equals 去比较 Key,而且空间利用率最大。

HashMap 采用了一个分两步走的哈希策略来保证分布的均匀:

  • 使用 hash() 方法用于对 Key 的 hashCode 进行重新计算,以防止质量低下的 hashCode()函数实现。由于 hashMap 的支撑数组长度总是 2 的倍数,通过右移可以使低位的数据尽量的不同,从而使 Key 的 hash 值的分布尽量均匀;
  • 使用 indexFor() 方法进行取余运算,以使 Entry 对象的插入位置尽量分布均匀。

因此,总的来说,HashMap 的底层数组长度总是 2 的 n 次方的原因有两个,即当 length=2^n 时:

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