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也相同。