HashMap实现了Map接口,是基于哈希表的非同步实现,它以键值对(key-value)的形式存储元素,键和值都可以为null。HashMap不保证映射的顺序,特别是它不保证该顺序不变。

HashMap底层实现

它的底层是通过数组实现的,数组的每个元素是链表,由Entry内部类实现,Entry重要的属性有 key , value, next。

其中Java源码如下:

transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
    ……
}

可以看出,Entry就是数组中的元素,每个 Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

HashMap构造函数

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

HashMap有3个构造函数,以上是无参数的构造函数,它们完成的工作就是对loadFactor和threshold这两个成员属性赋值。

threshold: 初始容量,表示哈希表中桶的数量,即初始时数组的大小。
loadFactor:负载因子,默认为0.75,表示当前哈希表的最大填满比例。当threshold * loadFactor < 当前哈希表中桶数目时,哈希表的threshold需要扩大为当前的2倍。

HashMap的存取实现

存储时,HashMap其实首先会判断key是否为null,如果为null, 则将该key-value键值对插入到table中索引为0的位置;

如果key不为null,则计算key的hash值,之后根据该hash值生成它在table中的下标索引。遍历此下标对应的链表,看是否存在该key值,有则替换,无则在链表头部插入新的元素。

存储:put(key, value)

public V put(K key, V value) {
    //对table数组分配内存,可以看出HashMap只有在存储元素时才会分配内存
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 当key为null时,调用putForNullKey方法,将该key保存在table数组下标为0的位置上。
    if (key == null)
        return putForNullKey(value);
    // 计算key的hash值
    int hash = hash(key);
    // 计算插入数据所在链表在table中的下标(内部实现很奇妙,下次来分析这个好了↖(^ω^)↗~)
    int i = indexFor(hash, table.length);
    // 遍历此下标对应的链表,看是否存在该key值
    for (Entry < K, V > e = table[i]; e != null; e = e.next) {
        Object k;
        // 判断链表上是否有相同hash值的entry,有则替换entry的value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            // 返回旧值,结束插入操作
            return oldValue;
        }
    }
    // 在下标i对应的链表中没有找到key相同的Entry,则创建一个新的Entry,进行插入操作
    modCount++;
    // 在下标为i的链表的头部进行插入操作(放在最后的话需要遍历单链表,消耗内存)
    addEntry(hash, key, value, i);
    return null;
}

读取时,判断key是否为null,如果是则直接查找下标为0的链表中key为null的Entry的value值;

否则,计算key的hash值,根据hash值找到它在table中的索引,遍历该索引对应的链表,查找key值对应的Entry的value值。

读取:get(key)

public V get(Object key) {
    // 若key为null,调用getForNullKey方法,即查找下标为0的链表中key为null的Entry的value,如果未找到则返回null
    if (key == null)
        return getForNullKey();
    //获取key对应的Entry对象
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
    // 计算key的hash值
    int hash = (key == null) ? 0 : hash(key);
    //根据key的hash值计算在table中的索引
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
        e != null;
        e = e.next) {
        Object k;
        //先判断e.hash == hash,过滤大量不符合的节点,然后对剩下的节点继续判断
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
        }
        return null;
    }

最后一个if里面的判断条件,(k = e.key) == key || (key != null && key.equals(k))牵扯出一个很经典的经验:重写equals方法时一定要重写hashCode方法。

因为默认对象的hashCode值一般是内存地址对应的数字,所以不同的对象其hashCode值一般不同。

所以当重写equals方法时,如果我们想让两个对象逻辑上相同,不重写hashCode的话,就会在HashMap中出现问题,两个对象明明逻辑上相同,但是因为默认hashCode值不同,就会认为两个对象不同。所以当用HashMap存储对象的时候,一定要重写hashCode()方法。

hashCode()方法重写规则:当equlas方法返回true时,两个对象的hashCode也相同。