构造函数

构造函数

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

    ......

}

其中,EntryHashMap的内部类,实现了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的底层数组长度为何总是2n次方?

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

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

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

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

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

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

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