结构
数组+链表
- 数组:key-value形式的Entry数组
- 链地址法解决哈希冲突,相同桶位的元素咦链表形式连接存储
// 表,根据需要调整大小。长度必须是2的幂。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // 数组
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next; // 链表
int hash;
}
创建
// 构造一个具有指定初始容量和加载因子的空HashMap
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
// 构造一个空HashMap,具有指定的初始容量和默认加载因子(0.75)
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 构造一个具有默认初始容量(16)和默认负载系数(0.75)的空HashMap
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* 使用与指定Map相同的映射构造一个新的HashMap
* HashMap使用默认的装载因子(0.75)创建,初始容量足以容纳指定映射中的映射
*/
public HashMap(Map<? extends K, ? extends V> m) {
// 创建一个指定容量,默认负载因子的HashMap
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 初始化Entry[]数组
inflateTable(threshold);
// 将m中的元素转移到HashMap中
putAllForCreate(m);
}
默认创建一个容量是16、负载因子是0.75的HashMap,初始Entry[]数组为空{},在第一次进行put操作插入元素时完成Entry[]的初始化
put操作
/**
* 将指定的值与此映射中的指定键关联
* 如果该映射先前包含了该键的映射,则旧值将被替换
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
// 初始化Entry[]数组
inflateTable(threshold);
}
// 添加null键的entry
if (key == null)
return putForNullKey(value);
// 计算key的哈希值,hash()方法保证key的hashcode计算出来后散列性更强
int hash = hash(key);
// 计算出在entry[]的索引位置
int i = indexFor(hash, table.length);
// 遍历entry[i]位置的链表,判断是否已经存在该key,存在即覆盖
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 覆盖相同key的entry的value值,并返回旧的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;
}
}
// 记录修改次数+1
modCount++;
// 添加元素到map中
addEntry(hash, key, value, i);
return null;
}
putForNullKey
支持一个null键,多个null值
private V putForNullKey(V value) {
// 使用Entry[]的0号位置存放null键的entry
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
// HashMap中已经存在null键entry
if (e.key == null) {
// 覆盖null键的entry值
V oldValue = e.value;
e.value = value;
// 每当对HashMap中已经存在的键k调用put(k,v)覆盖条目中的值时,就会调用这个方法
e.recordAccess(this);
return oldValue;
}
}
// 修改次数+1,用于多线程环境下数据被其他修改后快速抛出异常,fail-fast
modCount++;
// 添加元素到map中
addEntry(0, null, value, 0);
return null;
}
addEntry
// 添加元素到map中
void addEntry(int hash, K key, V value, int bucketIndex) {
// 判断是否需要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容,容量*2
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
// 计算key应该存储在新entry[]的桶位
bucketIndex = indexFor(hash, table.length);
}
// 添加entry到集合中
createEntry(hash, key, value, bucketIndex);
}
createEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
// 该桶位头节点
Entry<K,V> e = table[bucketIndex];
// 插入该桶位中,next指针指向之前的头节点
table[bucketIndex] = new Entry<>(hash, key, value, e);
// 记录集合中元素个数加1
size++;
}
扩容操作
- Entry[]容量扩大2倍
- 将旧Entry[]元素移动到新的Entry[]数组中(是否rehash)
resize
该方法主要用于扩容
/**
* 将此map的内容重新散列到一个具有更大容量的新数组中
* 当此映射中的键数达到其阈值时,将自动调用此方法
* 如果当前容量为MAXIMUM_CAPACITY,该方法不会调整映射的大小,而是将阈值设置Integer.MAX_VALUE
*/
void resize(int newCapacity) {
// 存放旧的entry[]数值
Entry[] oldTable = table;
// 存放旧的entry[]数组容量
int oldCapacity = oldTable.length;
// 旧的数组容量 == 最大容量
if (oldCapacity == MAXIMUM_CAPACITY) {
// 扩容阈值设置为最大整数
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个容量为newCapacity的新entry[]数组
Entry[] newTable = new Entry[newCapacity];
// 将旧entry[]数组中的元素迁移到新的entry[]数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 设置HashMap的table变量指向新的entry[]数组
table = newTable;
// 重新设置扩容阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer
该方法主要用于扩容时把旧entry数组中的全部元素迁移迁移到新的entry数组中
void transfer(Entry[] newTable, boolean rehash) {
// 新entry[]数组的容量
int newCapacity = newTable.length;
// entry数组的每个桶位
for (Entry<K,V> e : table) {
// 每个桶位中的链表遍历
while(null != e) {
// 链表的下一个entry节点
Entry<K,V> next = e.next;
// rehash绝大多数情况下都是false
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 计算出entry节点在新的entry[]数组中的索引
int i = indexFor(e.hash, newCapacity);
// 头插法将entry节点插入新entry[]数组中(这是多线程情景下导致成环的主要原因)
e.next = newTable[i];
newTable[i] = e;
// 遍历下一个节点
e = next;
}
}
}
扩容一般是扩大2倍,这样的好处是:
容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突。所以扩容必须2倍就是为了维持容量始终为2的幂次方
原先容量为a,现在容量为2a,那么原先索引为x,现在索引为x或者(x+a)
get操作
// 返回指定键映射到的值,如果该map不包含该键的映射,则返回null
public V get(Object key) {
// key为null的查找
if (key == null)
return getForNullKey();
// 根据key查找对应的entry节点
Entry<K,V> entry = getEntry(key);
// 从entry节点中获取对应的value值
return null == entry ? null : entry.getValue();
}
getForNullKey
/**
* 专门查找空键的方法。Null键映射到索引0
*
* 为了在两种最常用的操作(get和put)中提高性能,这个空情况被拆分为单独的方法,
* 但在其他操作中与条件合并
*/
private V getForNullKey() {
// map中没有元素,直接返回null
if (size == 0) {
return null;
}
// table数组0号位置冲突链上查找key为null的entry对应的value值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
getEntry
// 根据key在HashMap中查找相关联的entry
final Entry<K,V> getEntry(Object key) {
// map中没有元素,直接返回null
if (size == 0) {
return null;
}
// 计算key的哈希值
int hash = (key == null) ? 0 : hash(key);
// 找到table索引位置,遍历该索引位置的冲突链
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 找到对应的key的entry,返回这个entry节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}